Curry Leaves — a small, provider-agnostic, multi-agent kernel for building AI agents of any kind: streaming tool-use loop, sub-agents, skills, thinking. The Python port.
Project description
Curry Leaves Agent Loop
A small, provider-agnostic, multi-agent kernel for building AI agents of any kind — in clean, readable Python.
Curry Leaves is a general-purpose agent kernel small enough to read in an afternoon. At its core is a streaming tool-use loop — call the model, run the tools it asks for, feed the results back, repeat — with everything a real agent needs built around it: sub-agents, skills, MCP tools, permission gating, session recording, automatic context compaction, and per-turn reasoning-effort sizing. Point it at any tools and any task; the kernel is domain-agnostic — it just happens to ship a batteries-included coding toolset and CLIs on top.
One engine, any provider, any UI. The loop knows nothing about a specific LLM wire format — each provider translates at its own boundary — so Anthropic, OpenAI, and Ollama are a config choice, not branching logic. Use it as a library (below), or launch either bundled CLI: a full-screen terminal UI or a line REPL.
from curry_leaves import Agent, Runner, coding_tools
agent = Agent(model="claude-sonnet-4-5", tools=coding_tools())
result = await Runner(agent).run("Summarize README.md in three bullets.")
print(result.output_text)
Table of contents
- Features
- Install
- Quick start
- Configure a provider
- Terminal UI & REPL
- Library usage
- Environment variables
- Architecture
- Project layout
- Development
- Contributing
- Non-goals
- Acknowledgements
- License
Features
- Multi-provider. Anthropic, OpenAI, and Ollama (any OpenAI-compatible gateway) implement one
Provider.stream()interface; the loop never changes. Streaming SSE is assembled into a neutralAssistantMessageat the provider boundary and nowhere else. - Rich toolset.
read,write,edit,find,search,bash,task_create/task_update/task_list/task_get,ask,current_time,web_fetch,web_search. Tool args are pydantic models → JSON Schema for free. Oversized output is offloaded to an artifact store the model can page through withread. - MCP tools. Connect any Model Context Protocol server —
stdio subprocess or HTTP — and pick specific tools by name with
mcp_tools(); the result is a plainlist[Tool]spliced intoAgent(tools=[...])like any preset. Servers can also be declared insettings.json'smcpServerskey. MCP tools default torisk="exec", so they always go through the permission gate. - Deferred tools +
search_tools. Keep the advertised list lean; the model discovers more tools by keyword and activates them for the next turn — works across providers. - Sub-agents. Declare
subagents=[...]; the parent gets atasktool (delegation that returns a result) and atransfertool (one-way handoff). Bounded recursion depth. - Structured output. Give an agent an
output_type(a pydantic model); the Runner injects the schema, validates the final reply, and retries on mismatch.result.outputis typed. - Auto-thinking.
auto_thinking=Truesizes reasoning effort (Anthropic thinking budget / OpenAI reasoning effort) per turn with a cheap classifier. - Skills. Drop a
SKILL.mdunder.curry-leaves/skills/<name>/; its teaser goes into the prompt and the model pulls the full body viaread skill://<name>only when relevant (progressive disclosure). - Permissions. An opt-in gate authorizes each tool call (
allow/ask/deny), with standing approvals and contained-change auto-approval. Off by default — headless runs never hang. - Sessions. Each run can be recorded to
<home>/sessions/<id>/(meta.json+transcript.jsonl). Branch a new session off an existing one withfork_session()(or/fork [n]in the CLIs) — it replays the recorded transcript into a fresh, independent session that diverges from there. - Compaction. Long conversations are summarized as they near the context window — automatic, or
on demand via
Runner.compact()/ the/compactcommand. - Elision (opt-in). Before compaction ever fires, stale tool results (superseded reads, output
untouched for N turns) are swapped for short stubs — originals preserved in the blob store, one
read artifact://<id>away. Lossless, deterministic (no model call), and batched into sweeps so it never thrashes the provider's prompt cache.RunConfig(elision=ElisionConfig(enabled=True)). - Typed, async, dependency-light. Python 3.11+,
mypy --strict, shipspy.typed. The library core rides onpydantic+httpx; the CLIs add Textual + Rich, and MCP support uses the officialmcpSDK.
Install
pip install curry-leaves
Then launch a CLI:
curry-leaves # full-screen terminal UI (alias: curry)
curry-leaves-repl # line REPL (works with piped input)
Requires Python 3.11+.
Quick start
import asyncio
from curry_leaves import Agent, Runner, coding_tools
async def main() -> None:
agent = Agent(
model="claude-sonnet-4-5",
instructions="You are a concise coding assistant.",
tools=coding_tools(),
)
result = await Runner(agent).run("What does src/curry_leaves/runner.py do?")
print(result.output_text)
asyncio.run(main())
Agent is a stateless definition (model, tools, instructions); Runner holds the live
conversation and drives the streaming loop. Set an API key first (see below).
Configure a provider
export ANTHROPIC_API_KEY=sk-ant-... # or
export OPENAI_API_KEY=sk-...
The provider is inferred from the model id (claude-* → Anthropic, gpt-*/o1-* → OpenAI,
gemma*/llama*/qwen*/… → Ollama), or set CURRY_LEAVES_PROVIDER / pass an explicit provider
to the Agent.
Local models via Ollama
Ollama speaks the OpenAI wire format, so any pulled tag works with no API key:
ollama pull qwen3
CURRY_LEAVES_MODEL=qwen3 curry-leaves-repl
from curry_leaves import Agent, Runner, coding_tools
agent = Agent(model="qwen3", tools=coding_tools()) # provider → Ollama
Set OLLAMA_HOST to point at a non-default server (http://host:port). Tool use needs a model with
the tools capability (qwen3, llama3.x, gemma3, …). Reasoning-effort knobs are dropped for Ollama,
and usage cost is $0 (local). You can point the same OpenAIProvider at any OpenAI-compatible
gateway via OPENAI_BASE_URL or explicit provider options.
Terminal UI & REPL
A full-screen terminal UI (built on Textual) ships with the package — a header bar, a streaming transcript, live thinking blocks, spinner-tracked tool calls, indented sub-agent activity, a status bar, and a persistent input box. Finished turns land in real terminal scrollback; the active turn streams in place.
curry-leaves
╭──────────────────────────────────────────────────────────╮
│ curry-leaves · claude-sonnet-4-5 (anthropic) │
│ /repo · 11 tools · subagents: explore, plan │
╰──────────────────────────────────────────────────────────╯
you › what does src/curry_leaves/runner.py do?
ai ›
→ read({"path":"src/curry_leaves/runner.py"})
1 """The Runner — holds one live conversation … """
The Runner composes an Agent with conversation state and drives the loop …
● ready in 4213 · out 187 · $0.0155
╭──────────────────────────────────────────────────────────╮
│ › ask anything — /help for commands │
╰──────────────────────────────────────────────────────────╯
Slash commands: /help, /reset, /fork [n] (branch a new session from this conversation,
optionally up through the nth user turn), /tools, /skills, /model, /stats, /clear,
/compact [focus], /auto (toggle contained-change auto-approve), /autonomous (toggle self-drive
mode), /exit. The TUI needs an interactive terminal (a TTY).
The line-streaming REPL is available as curry-leaves-repl — handy for piped / non-TTY input.
Library usage
Stream events instead of awaiting
async for event in runner.stream("Refactor the parser"):
if event.type == "message_update" and event.delta and event.delta.kind == "text":
print(event.delta.value, end="", flush=True)
Structured output
import pydantic
from curry_leaves import Agent, Runner
class Summary(pydantic.BaseModel):
title: str
bullets: list[str]
agent = Agent(model="claude-sonnet-4-5", output_type=Summary)
result = await Runner(agent).run("Summarize this repo.")
report = result.output # validated Summary instance; the Runner retries on a schema mismatch
Sub-agents
from curry_leaves import Agent, Runner, coding_tools, explore_agent, plan_agent
agent = Agent(
model="claude-sonnet-4-5",
tools=coding_tools(),
subagents=[explore_agent("claude-sonnet-4-5"), plan_agent("claude-sonnet-4-5")],
)
# The parent can `task` (delegate → result) or `transfer` (one-way handoff) to these.
MCP tools
from curry_leaves import Agent, Runner, coding_tools
from curry_leaves.mcp import McpServerStdio, mcp_tools
async with McpServerStdio(name="github", command="npx", args=["-y", "@modelcontextprotocol/server-github"]) as gh:
agent = Agent(
model="claude-sonnet-4-5",
tools=[*coding_tools(), *await mcp_tools(gh, "search_issues")],
)
result = await Runner(agent).run("Find open issues about streaming.")
Or declare servers once in .curry-leaves/settings.json under mcpServers and load them with
load_mcp_servers() + McpServerManager. See
examples/07_mcp_tools.py for a complete, self-contained program.
See examples/01_basic.py for the smallest runnable program, and
examples/06_host_and_permissions.py for a self-contained
illustration of the host / permission model.
Environment variables
| Variable | Purpose | Default |
|---|---|---|
ANTHROPIC_API_KEY |
Anthropic auth | — |
OPENAI_API_KEY |
OpenAI auth | — |
CURRY_LEAVES_MODEL |
Model id for the CLIs | auto-detected |
CURRY_LEAVES_PROVIDER |
Force a provider (anthropic | openai | ollama) |
inferred from model id |
CURRY_LEAVES_HOME |
Base dir for settings / skills / sessions | ~/.curry-leaves |
CURRY_LEAVES_NO_RECORD |
Disable session recording when set | recording on |
OPENAI_BASE_URL |
Point OpenAIProvider at any OpenAI-compatible gateway |
https://api.openai.com/v1 |
OLLAMA_HOST |
Ollama server URL | http://localhost:11434 |
NO_COLOR |
Disable ANSI color | color on |
Architecture
The design is a strict layering — a stateless definition on a stateful driver on a pure engine — with all I/O pushed to swappable seams (Provider, Host, Tool).
| Layer | File | Responsibility |
|---|---|---|
| Message model | core/messages.py |
Provider-neutral Message/Content types — the one thing everything agrees on. |
| Events | core/events.py |
What the loop yields; a small structural set + a streaming delta payload. |
| Loop | core/loop.py |
The pure engine: stream → run tools → stream, while tools are called. Yields events. |
| Tools | core/tools.py |
A registry of pydantic-typed tools + a concurrent executor with a universal large-result guard. |
| Agent | core/agent.py |
A stateless definition (model, tools, instructions, sub-agents, output_type). |
| Runner | runner.py |
Live conversation state; builds the Context each turn; wires sub-agents, handoff, permissions, compaction. |
| Providers | providers/* |
The only place that knows a wire format. Anthropic / OpenAI / Ollama behind one Provider. |
| MCP | mcp/* |
MCP client: stdio/HTTP server connections, a manager, a settings loader, and a tool adapter. |
| Host | core/host.py |
The frontend seam: emit(event) + request(req). Headless by default. |
| Prompt | prompt.py |
Layered system prompt (identity → instructions → env → context → tools), cache-friendly. |
| Permission | permission.py |
Per-call allow / ask / deny gate with standing approvals. |
| Thinking | thinking.py |
A tiny classifier that sizes reasoning effort per task. |
| Skills | skills.py |
Progressive-disclosure capability packages via skill:// refs. |
| Compaction | compaction.py |
Summarizes old history as the context window fills. |
| Elision | elision.py |
Stubs out stale tool results (recoverably) before compaction is needed. |
The one decision that drives the whole loop:
runnable = stop_reason in ("tool_use", "stop") AND tool_calls exist
Tools were called → loop again. None → stop.
Project layout
src/curry_leaves/
core/ # messages, events, loop, tools, agent, host, blobs — the kernel
providers/ # anthropic, openai/ollama, factory, sse, base
tools/ # read, write, edit, find, search, bash, tasks, ask, web, …
mcp/ # MCP client: servers, manager, config, tool adapter
session/ # session store + recording + forking (replay a transcript into a new branch)
cli/ # chat REPL + Textual TUI
util/ # paths, retry, frontmatter, resources
runner.py prompt.py permission.py thinking.py skills.py compaction.py elision.py catalog.py settings.py
__init__.py # public API surface
examples/ # runnable examples
tests/ # pytest suite (MCP subsystem, session forking, elision)
Development
git clone https://github.com/Curry-Leaves/curry-leaves-py.git
cd curry-leaves-py
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
mypy src # strict type check (the correctness gate)
pytest # run the test suite
curry-leaves # launch the Textual TUI (needs a TTY)
curry-leaves-repl # launch the REPL (works with piped input)
python3 examples/01_basic.py "What is this project?"
mypy --strict is the gate every change must pass; pytest covers the MCP subsystem.
Contributions extending the test suite are welcome.
Contributing
Contributions are very welcome — bug reports, features, docs, and tests. In short:
- Open an issue first for anything non-trivial, so we can agree on the approach.
- Fork & branch off
main(git checkout -b feat/short-description). - Make the change, keep the diff focused, and ensure
mypy srcandpytestpass. - Open a pull request describing the what and why.
See CONTRIBUTING.md for the full guide — dev setup, code conventions, how to add a tool / provider / frontend capability, commit style, and bug-reporting.
Non-goals
curry-leaves is deliberately small. It does not include LSP integration, vector stores, or a
plugin marketplace. If you need those, they belong in a layer built on top of the kernel, not inside
it. (Unlike the TypeScript sibling, MCP is included here — as a thin client layer that feeds the
existing Tool seam, not a change to the kernel.)
Acknowledgements
curry-leaves for Python is the sibling of curry-leaves-ts, a TypeScript kernel of the same design; the two ports mirror each other module-for-module.
License
MIT © Ilayanambi Ponramu
Project details
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 curry_leaves-1.3.1.tar.gz.
File metadata
- Download URL: curry_leaves-1.3.1.tar.gz
- Upload date:
- Size: 211.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a824bbabd2000f88ebc8289848d5951f7a06a7677897a09c6a16ea0311a7e01e
|
|
| MD5 |
349713cd0d230362eaac76a1f5be9e52
|
|
| BLAKE2b-256 |
27eed99414e4c3adb86556bc5795b3b572c22a98a7627377e430ec20b47889b1
|
File details
Details for the file curry_leaves-1.3.1-py3-none-any.whl.
File metadata
- Download URL: curry_leaves-1.3.1-py3-none-any.whl
- Upload date:
- Size: 156.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c93483ce694d60bbd7f8dbd449d560a4d2e932459233dcc21ee4196612be127
|
|
| MD5 |
3e779bf0858693f8ac076c91d5e8df86
|
|
| BLAKE2b-256 |
f8cc2906fe06ac9d74ca78557909b77957565bce646a0ed2dc2f11c75737137a
|