curry-leaves
v1.0.1
Published
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 TypeScript port.
Downloads
39
Maintainers
Readme
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, 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.
import { Agent, Runner, codingTools } from "curry-leaves";
const agent = new Agent({ model: "claude-sonnet-4-5", tools: codingTools() });
const result = await new Runner(agent).run("Summarize README.md in three bullets.");
console.log(result.outputText);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 zod schemas → JSON Schema for free. Oversized output is offloaded to an artifact store the model can page through withread. - 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
outputType(a zod schema); the Runner injects the schema, validates the final reply, and retries on mismatch.result.outputis typed. - Auto-thinking.
autoThinking: 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). - Compaction. Long conversations are summarized as they near the context window — automatic, or
on demand via
Runner.compact()/ the/compactcommand. - Typed, dependency-light, ESM. Node 20+. Ships
.d.tstypes. The library entry pulls in onlyzod; the CLIs add Ink + React.
Install
npm install curry-leavesOr use the CLIs without installing globally:
npx curry-leaves # full-screen terminal UI
npx curry-leaves-repl # line REPL (works with piped input)Requires Node 20+.
Quick start
import { Agent, Runner, codingTools } from "curry-leaves";
const agent = new Agent({
model: "claude-sonnet-4-5",
instructions: "You are a concise coding assistant.",
tools: codingTools(),
});
const result = await new Runner(agent).run("What does src/runner.ts do?");
console.log(result.outputText);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 gemma4
CURRY_LEAVES_MODEL=gemma4 npx curry-leaves-replimport { Agent, Runner, codingTools } from "curry-leaves";
const agent = new Agent({ model: "gemma4", tools: codingTools() }); // provider → OllamaSet OLLAMA_HOST to point at a non-default server (http://host:port). Tool use needs a model with
the tools capability (gemma4, qwen3.6, llama3.x, …). 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 { baseUrl, apiKey }.
Terminal UI & REPL
A full-screen terminal UI (built on Ink) 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.
npx curry-leaves╭──────────────────────────────────────────────────────────╮
│ curry-leaves · claude-sonnet-4-5 (anthropic) │
│ /repo · 11 tools · subagents: explore, plan │
╰──────────────────────────────────────────────────────────╯
you › what does src/runner.ts do?
ai ›
→ read({"path":"src/runner.ts"})
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, /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 (npx curry-leaves-repl) — handy for piped / non-TTY
input.
Library usage
Stream events instead of awaiting
for await (const event of runner.stream("Refactor the parser")) {
if (event.type === "message_update" && event.delta?.kind === "text") {
process.stdout.write(event.delta.value);
}
}Structured output
import { z } from "zod";
import { Agent, Runner } from "curry-leaves";
const agent = new Agent({
model: "claude-sonnet-4-5",
outputType: z.object({ title: z.string(), bullets: z.array(z.string()) }),
});
const { output } = await new Runner(agent).run("Summarize this repo.");
// ^ typed + validated; the Runner retries on a schema mismatchSub-agents
import { Agent, Runner, codingTools, exploreAgent, planAgent } from "curry-leaves";
const agent = new Agent({
model: "claude-sonnet-4-5",
tools: codingTools(),
subagents: [exploreAgent("claude-sonnet-4-5"), planAgent("claude-sonnet-4-5")],
});
// The parent can `task` (delegate → result) or `transfer` (one-way handoff) to these.See examples/basic.ts for a complete runnable program, and
examples/host-demo.ts 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 | claude-sonnet-4-5 |
| 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 |
| 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.ts | Provider-neutral Message/Content types — the one thing everything agrees on. |
| Events | core/events.ts | What the loop yields; a small structural set + a streaming delta payload. |
| Loop | core/loop.ts | The pure engine: stream → run tools → stream, while tools are called. Yields events. |
| Tools | core/tools.ts | A registry of zod-typed tools + a concurrent executor with a universal large-result guard. |
| Agent | core/agent.ts | A stateless definition (model, tools, instructions, sub-agents, outputType). |
| Runner | runner.ts | 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. |
| Host | core/host.ts | The frontend seam: emit(event) + request(req). Headless by default. |
| Prompt | prompt.ts | Layered system prompt (identity → instructions → env → context → tools), cache-friendly. |
| Permission | permission.ts | Per-call allow / ask / deny gate with standing approvals. |
| Thinking | thinking.ts | A tiny classifier that sizes reasoning effort per task. |
| Skills | skills.ts | Progressive-disclosure capability packages via skill:// refs. |
| Compaction | compaction.ts | Summarizes old history as the context window fills. |
The one decision that drives the whole loop:
runnable = stopReason in ("tool_use", "stop") AND tool_calls existTools were called → loop again. None → stop.
Project layout
src/
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, …
session/ # session store + recording
cli/ # chat REPL + Ink TUI
runner.ts prompt.ts permission.ts thinking.ts skills.ts compaction.ts catalog.ts settings.ts
index.ts # public API surface
examples/ # runnable examplesDevelopment
git clone https://github.com/ilayanambi-ponramu/curry-leaves-ts.git
cd curry-leaves-ts
npm install # runs the build via the `prepare` hook
npm run build # tsc → dist/
npm run typecheck # tsc --noEmit (the correctness gate)
npm run tui # build + launch the Ink TUI
npm run chat # build + launch the REPL
npm run example # build + run examples/basic.ts
npm run clean # rm -rf distThere is currently no test runner; npm run typecheck under strict (plus
noUncheckedIndexedAccess) is the gate. Contributions adding a 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
npm run typecheckpasses. - Open a pull request describing the what and why.
One gotcha worth calling out: this is pure ESM with NodeNext resolution, so every relative import
must carry a .js extension even though the source is .ts (e.g. import { Agent } from
"./core/agent.js").
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 MCP or LSP integration. If you need those, they belong in a layer built on top of the kernel, not inside it.
Acknowledgements
curry-leaves is a TypeScript port of a Python agent kernel of the same design, and follows its architecture closely.
License
MIT © Ilayanambi Ponramu
