bare-agent 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+
9
+ # Tooling caches
10
+ .ruff_cache/
11
+ .pytest_cache/
12
+ .ty_cache/
13
+
14
+ # Env / secrets
15
+ .env
16
+ .env.local
17
+
18
+ # OS
19
+ .DS_Store
20
+
21
+ # Studio (Next.js)
22
+ node_modules/
23
+ .next/
24
+ next-env.d.ts
25
+ *.tsbuildinfo
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Subrata Mondal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,256 @@
1
+ Metadata-Version: 2.4
2
+ Name: bare-agent
3
+ Version: 0.0.1
4
+ Summary: A framework-free agent runtime you can read, run, and leave. Own the loop, not the framework. Runs local on Ollama at $0 — or any frontier model.
5
+ Project-URL: Homepage, https://github.com/subratamondal1/bare-agent
6
+ Project-URL: Repository, https://github.com/subratamondal1/bare-agent
7
+ Author: Subrata Mondal
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,framework-free,litellm,llm,local-first,ollama,tool-calling
11
+ Requires-Python: >=3.12
12
+ Requires-Dist: litellm>=1.55
13
+ Requires-Dist: orjson>=3.10
14
+ Requires-Dist: pydantic-settings>=2.7
15
+ Requires-Dist: pydantic>=2.10
16
+ Requires-Dist: python-dotenv>=1.0
17
+ Requires-Dist: structlog>=24.4
18
+ Provides-Extra: api
19
+ Requires-Dist: fastapi>=0.115; extra == 'api'
20
+ Requires-Dist: httpx>=0.28; extra == 'api'
21
+ Requires-Dist: redis>=5; extra == 'api'
22
+ Requires-Dist: uvicorn[standard]>=0.32; extra == 'api'
23
+ Description-Content-Type: text/markdown
24
+
25
+ <p align="center">
26
+ <img src="https://raw.githubusercontent.com/subratamondal1/bare-agent/main/docs/assets/logo.png" width="96" alt="Bare Agent" />
27
+ </p>
28
+
29
+ <h1 align="center">Bare Agent</h1>
30
+
31
+ <p align="center">
32
+ <strong>Own the loop, not the framework.</strong>
33
+ </p>
34
+
35
+ <p align="center">
36
+ A framework-free agent runtime you can read, run, and leave — a small library you<br/>
37
+ import and call, plus a visual studio that ejects to plain Python with zero dependency on us.
38
+ </p>
39
+
40
+ <p align="center">
41
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat" alt="License: MIT"></a>
42
+ <img src="https://img.shields.io/badge/python-3.12%2B-blue?style=flat" alt="Python 3.12+">
43
+ <img src="https://img.shields.io/badge/tests-29%20passing-brightgreen?style=flat" alt="Tests: 29 passing">
44
+ <img src="https://img.shields.io/badge/local--first-Ollama-orange?style=flat" alt="Local-first">
45
+ <img src="https://img.shields.io/badge/studio-Next.js%2016-black?style=flat" alt="Studio: Next.js 16">
46
+ </p>
47
+
48
+ <p align="center">
49
+ <a href="https://github.com/subratamondal1/bare-agent/actions/workflows/ci.yml"><img src="https://github.com/subratamondal1/bare-agent/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
50
+ <a href="https://github.com/subratamondal1/bare-agent/stargazers"><img src="https://img.shields.io/github/stars/subratamondal1/bare-agent?style=flat&color=yellow" alt="Stars"></a>
51
+ <a href="https://github.com/subratamondal1/bare-agent/commits/main"><img src="https://img.shields.io/github/last-commit/subratamondal1/bare-agent?style=flat" alt="Last commit"></a>
52
+ </p>
53
+
54
+ <p align="center">
55
+ <a href="#features">Features</a> •
56
+ <a href="#quickstart">Quickstart</a> •
57
+ <a href="#the-studio">Studio</a> •
58
+ <a href="#how-it-works">How it works</a> •
59
+ <a href="#eject">Eject</a> •
60
+ <a href="#configuration">Configuration</a> •
61
+ <a href="#development">Development</a>
62
+ </p>
63
+
64
+ ---
65
+
66
+ Most agent frameworks own your `main()`, hide control flow behind metaclasses and DAG executors,
67
+ and obscure the actual prompts. `bare-agent` is the opposite: a small library — the agent loop, a
68
+ tool registry, a 3-axis budget, and a LiteLLM gateway, ~600 readable lines — that you **import and
69
+ call**. You own the loop. Every prompt is in plain sight. You can always **eject to plain Python**
70
+ and run it with **zero `bare_agent` dependency**.
71
+
72
+ On top of the library sits an optional **visual studio**: wire agents into a chain on a canvas,
73
+ attach tools, **Run** and watch tokens stream live, then eject the whole flow to a self-contained
74
+ `agent.py`. **Local-first** — it runs at zero cost on Ollama; OpenAI, Anthropic, and Gemini are
75
+ optional drop-ins through the same loop. Built on Python 3.12 · LiteLLM · FastAPI · Next.js 16 —
76
+ with **no agent framework** (no LangChain/LangGraph): the loop, the budget, and the failure
77
+ handling are owned directly.
78
+
79
+ <p align="center">
80
+ <img src="https://raw.githubusercontent.com/subratamondal1/bare-agent/main/docs/assets/bare-agent-demo.gif" width="100%" alt="Bare Agent studio: chain a Solver and an Explainer agent on a canvas, attach the calculator, Run and watch each agent's turns, tool calls, and tokens stream live with real per-call cost, then Eject the whole flow to a self-contained Python script." />
81
+ </p>
82
+
83
+ <p align="center">
84
+ <em>The studio, end to end: chain a <strong>Solver</strong> and an <strong>Explainer</strong>, attach the calculator, <strong>Run</strong> and watch each agent stream its turns, tool calls, and tokens live — with real per-call cost attribution (here on <code>gpt-5.4-mini</code>, ~$0.0006 for the whole chain) — then <strong>Eject to Python</strong>, a self-contained <code>agent.py</code> with zero <code>bare_agent</code> dependency. The same loop runs local-first on Ollama at $0.</em>
85
+ </p>
86
+
87
+ ## Features
88
+
89
+ | Capability | Detail |
90
+ |---|---|
91
+ | **Framework-free agent loop** | A hand-written tool-use loop over LiteLLM with a 3-axis budget (turns / tokens / wall-clock) + hard cost cap, a retry/fallback ladder, and a self-registering, permission-gated tool registry. The loop is a stateless reducer over an explicit `messages: list[dict]`. |
92
+ | **Local-first, $0 — or BYO frontier key** | Every call goes through LiteLLM, so the model id picks the provider. `ollama_chat/qwen3` runs free and offline; `anthropic/…`, `openai/…`, `gemini/…` are drop-ins. No lock-in. |
93
+ | **Multi-agent chains** | Wire agents agent→agent; the runtime topologically orders them and feeds each answer into the next. Inline runs, queued runs, and ejected code all execute the same chain. |
94
+ | **Visual studio** | A React Flow canvas (Next.js 16 / React 19) to build chains, attach tools, and watch turns / tool calls / tokens stream live over SSE — one readable section per agent. |
95
+ | **Eject to plain Python** | Compile any graph to a standalone `agent.py` (litellm + pydantic only) — tool sources inlined, **zero `bare_agent` import**. Machine-checked to compile. The graph is a convenience, never a cage. |
96
+ | **HITL / permissions** | An `Approver` gates tool calls allow / ask / deny; successful tool output is wrapped `<untrusted_tool_output>` for prompt-injection containment. |
97
+ | **Horizontal scale** | An optional Redis-list job queue + worker pool; Kubernetes + **KEDA scale workers 0→N→0** on queue depth — the same shape as [Argus](https://github.com/subratamondal1/argus)'s searcher fan-out. |
98
+ | **Composition, not configuration** | Seams are Python `Protocol`s — swap the LLM, the approver, or the event sink by passing a different object. No god-object to subclass. |
99
+
100
+ ## Quickstart
101
+
102
+ ```bash
103
+ uv add bare-agent # or: pip install bare-agent
104
+ ```
105
+
106
+ A complete agent in ~30 lines — the docstring becomes the LLM's tool description:
107
+
108
+ ```python
109
+ import asyncio
110
+ from pydantic import BaseModel, Field
111
+ from bare_agent import AgentLoop, Budget, LLMClient, ToolRegistry, get_settings
112
+
113
+ registry = ToolRegistry()
114
+
115
+ class AddArgs(BaseModel):
116
+ a: int = Field(description="first addend")
117
+ b: int = Field(description="second addend")
118
+
119
+ @registry.tool()
120
+ async def add(args: AddArgs) -> int:
121
+ """Add two integers and return their sum."""
122
+ return args.a + args.b
123
+
124
+ async def main() -> None:
125
+ settings = get_settings() # local Ollama by default; set BARE_AGENT_MODEL for frontier
126
+ agent = AgentLoop(
127
+ registry=registry,
128
+ llm=LLMClient.from_settings(settings),
129
+ budget=Budget.from_settings(settings),
130
+ system_prompt="You are a precise assistant. Use tools for arithmetic.",
131
+ )
132
+ result = await agent.run("What is 17 + 25, then add 100 to that?")
133
+ print(result.answer) # -> "142"
134
+ print(result.stop_reason, result.turns, f"${result.cost_usd}") # -> completed 3 $0.0
135
+
136
+ asyncio.run(main())
137
+ ```
138
+
139
+ Run it locally for free:
140
+
141
+ ```bash
142
+ ollama pull qwen3 # one-time (qwen3:30b-a3b-thinking on a 32GB Mac)
143
+ make demo # or: uv run python examples/quickstart.py
144
+ ```
145
+
146
+ ## The studio
147
+
148
+ ```bash
149
+ make web # FastAPI on :8000 + Next.js studio on :3000 → http://localhost:3000/studio
150
+ ```
151
+
152
+ Open `http://localhost:3000/studio`: **Add** agents and wire them into a chain, attach catalog
153
+ tools, pick a model (local qwen3 at $0 or your frontier key), and **Run** — each agent streams its
154
+ turns, tool calls, and tokens live over SSE in its own section. The backend is standalone: `make
155
+ api` runs the control plane alone, and the library works with no UI at all.
156
+
157
+ ## How it works
158
+
159
+ ```
160
+ user input
161
+
162
+
163
+ ┌──────────────┐ answer feeds ┌──────────────┐
164
+ │ Agent 1 │ ───────────────► │ Agent 2 │ ──────────► final answer
165
+ │ + tools │ the next │ + tools │
166
+ └──────────────┘ └──────────────┘
167
+ each agent = ONE hand-written loop:
168
+ explicit messages list · 3-axis budget + cost cap · permission-gated tool dispatch
169
+
170
+ run it: inline over SSE · or queue → worker pool → KEDA scales 0→N→0
171
+ keep it: Eject ──► agent.py (litellm + pydantic only — ZERO bare_agent dependency)
172
+ ```
173
+
174
+ The loop is a **stateless reducer** over an explicit `messages: list[dict]`. That one decision pays
175
+ three ways, all for free:
176
+
177
+ - **Durability** — the list is serializable, so checkpoint it and resume after a crash.
178
+ - **Eject-to-code** — the list *is* the program; there was never a framework underneath to lift out.
179
+ - **Testability** — feed a canned `messages` list (or a fake `CompletionClient`), assert.
180
+
181
+ No metaclass magic, no hidden DAG executor, no god-object to subclass, no state trapped in a
182
+ session. Extensibility is composition: `AgentLoop(llm=..., approver=..., registry=...)`.
183
+
184
+ ### The 8 primitives (each usable on its own — not a god-object)
185
+
186
+ | # | Primitive | Where |
187
+ |---|---|---|
188
+ | ① | Tool registry — `@registry.tool()` → JSON-schema → permission-gated dispatch | `registry.py` |
189
+ | ② | Prompt assembly — the explicit, serializable `messages: list[dict]` | `loop.py` |
190
+ | ③ | Agent loop — `AsyncExitStack` + 3-axis budget + termination + cycle-stop | `loop.py` |
191
+ | ④ | Retry / fallback over LiteLLM (local Ollama **or** any frontier model) | `llm.py` |
192
+ | ⑤ | State / memory — checkpoint the `messages` list (durability for free) | `loop.py` |
193
+ | ⑥ | HITL / permissions — allow / ask / deny, an `Approver` on `ask` | `registry.py` |
194
+ | ⑦ | Observability — `structlog` + an optional `EventSink` (SSE-ready) | `events.py` |
195
+ | ⑧ | Eval gate — golden replay (roadmap) | — |
196
+
197
+ ## Eject
198
+
199
+ Any flow — single agent or a chain — compiles to a standalone script that imports only `litellm`
200
+ and `pydantic`. Tool sources are inlined verbatim; there is **no `bare_agent` import**:
201
+
202
+ ```bash
203
+ uv run --with litellm --with pydantic agent.py "your question"
204
+ ```
205
+
206
+ In the studio, **Eject to Python** shows the generated code and downloads it. The generated file is
207
+ machine-checked to compile. You can read it, diff it, vendor it, and run it after you stop using
208
+ bare-agent entirely — that is the point.
209
+
210
+ ## Configuration
211
+
212
+ Settings are read by [Pydantic Settings](src/bare_agent/config.py) from the environment
213
+ (`BARE_AGENT_` prefix) or `.env` (`cp .env.example .env`). The defaults are fully local and free.
214
+ Common overrides:
215
+
216
+ | Variable | Default | Purpose |
217
+ |---|---|---|
218
+ | `BARE_AGENT_MODEL` | `ollama_chat/qwen3` | LiteLLM model id. Local Ollama by default; `anthropic/…`, `openai/…`, `gemini/…` for hosted. |
219
+ | `BARE_AGENT_OLLAMA_BASE_URL` | `http://localhost:11434` | Ollama server, passed as `api_base` for `ollama_chat/` models. |
220
+ | `BARE_AGENT_FALLBACK_MODELS` | `[]` | Ordered fallback model ids (JSON list) for the retry ladder. |
221
+ | `BARE_AGENT_MAX_TURNS` / `…_TOKENS` / `…_WALLCLOCK_S` / `…_COST_USD` | `8` / `120000` / `180` / `0.50` | The 3-axis budget + hard cost cap; the loop stops on the first to trip. |
222
+ | `BARE_AGENT_USE_QUEUE` | `false` | Route runs through the Redis queue + worker pool (KEDA-autoscalable) instead of inline. |
223
+ | `BARE_AGENT_REDIS_URL` | `redis://localhost:6379/0` | Redis DSN for the run queue + event pub/sub (queue mode). |
224
+
225
+ For a hosted model, set `BARE_AGENT_MODEL=anthropic/…` and export that provider's key
226
+ (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`) — LiteLLM reads it from the environment.
227
+
228
+ ## Development
229
+
230
+ ```bash
231
+ make ci # lock-check + format-check + lint (ruff) + compile + typecheck (ty) + tests (pytest)
232
+ make test # the 29-test suite — hermetic (the LLM and Redis are faked; no daemon needed)
233
+ make web # backend + studio together for local hacking
234
+ make up / down # the Docker stack (api + studio; Ollama stays on the host)
235
+ make queue-up # the Docker stack WITH the KEDA-shaped worker plane (+ redis + worker)
236
+ make help # all targets
237
+ ```
238
+
239
+ Kubernetes manifests live in [`k8s/`](k8s/) — an inline deploy (api + studio) and the KEDA worker
240
+ plane (redis + worker). The studio has its own toolchain ([`apps/studio/AGENTS.md`](apps/studio/AGENTS.md));
241
+ the canonical agent rules for the whole repo are in [`AGENTS.md`](AGENTS.md).
242
+
243
+ <!-- Uncomment once the repo has stars (renders an empty chart at 0):
244
+ ## Star history
245
+
246
+ <p align="center">
247
+ <a href="https://star-history.com/#subratamondal1/bare-agent&Date">
248
+ <img src="https://api.star-history.com/svg?repos=subratamondal1/bare-agent&type=Date" width="600" alt="Star history">
249
+ </a>
250
+ </p>
251
+ -->
252
+
253
+ ## License
254
+
255
+ MIT © 2026 Subrata Mondal — see [LICENSE](LICENSE). Built as the clean, reusable extraction of
256
+ [Argus](https://github.com/subratamondal1/argus)'s agent runtime.
@@ -0,0 +1,232 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/subratamondal1/bare-agent/main/docs/assets/logo.png" width="96" alt="Bare Agent" />
3
+ </p>
4
+
5
+ <h1 align="center">Bare Agent</h1>
6
+
7
+ <p align="center">
8
+ <strong>Own the loop, not the framework.</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ A framework-free agent runtime you can read, run, and leave — a small library you<br/>
13
+ import and call, plus a visual studio that ejects to plain Python with zero dependency on us.
14
+ </p>
15
+
16
+ <p align="center">
17
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat" alt="License: MIT"></a>
18
+ <img src="https://img.shields.io/badge/python-3.12%2B-blue?style=flat" alt="Python 3.12+">
19
+ <img src="https://img.shields.io/badge/tests-29%20passing-brightgreen?style=flat" alt="Tests: 29 passing">
20
+ <img src="https://img.shields.io/badge/local--first-Ollama-orange?style=flat" alt="Local-first">
21
+ <img src="https://img.shields.io/badge/studio-Next.js%2016-black?style=flat" alt="Studio: Next.js 16">
22
+ </p>
23
+
24
+ <p align="center">
25
+ <a href="https://github.com/subratamondal1/bare-agent/actions/workflows/ci.yml"><img src="https://github.com/subratamondal1/bare-agent/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
26
+ <a href="https://github.com/subratamondal1/bare-agent/stargazers"><img src="https://img.shields.io/github/stars/subratamondal1/bare-agent?style=flat&color=yellow" alt="Stars"></a>
27
+ <a href="https://github.com/subratamondal1/bare-agent/commits/main"><img src="https://img.shields.io/github/last-commit/subratamondal1/bare-agent?style=flat" alt="Last commit"></a>
28
+ </p>
29
+
30
+ <p align="center">
31
+ <a href="#features">Features</a> •
32
+ <a href="#quickstart">Quickstart</a> •
33
+ <a href="#the-studio">Studio</a> •
34
+ <a href="#how-it-works">How it works</a> •
35
+ <a href="#eject">Eject</a> •
36
+ <a href="#configuration">Configuration</a> •
37
+ <a href="#development">Development</a>
38
+ </p>
39
+
40
+ ---
41
+
42
+ Most agent frameworks own your `main()`, hide control flow behind metaclasses and DAG executors,
43
+ and obscure the actual prompts. `bare-agent` is the opposite: a small library — the agent loop, a
44
+ tool registry, a 3-axis budget, and a LiteLLM gateway, ~600 readable lines — that you **import and
45
+ call**. You own the loop. Every prompt is in plain sight. You can always **eject to plain Python**
46
+ and run it with **zero `bare_agent` dependency**.
47
+
48
+ On top of the library sits an optional **visual studio**: wire agents into a chain on a canvas,
49
+ attach tools, **Run** and watch tokens stream live, then eject the whole flow to a self-contained
50
+ `agent.py`. **Local-first** — it runs at zero cost on Ollama; OpenAI, Anthropic, and Gemini are
51
+ optional drop-ins through the same loop. Built on Python 3.12 · LiteLLM · FastAPI · Next.js 16 —
52
+ with **no agent framework** (no LangChain/LangGraph): the loop, the budget, and the failure
53
+ handling are owned directly.
54
+
55
+ <p align="center">
56
+ <img src="https://raw.githubusercontent.com/subratamondal1/bare-agent/main/docs/assets/bare-agent-demo.gif" width="100%" alt="Bare Agent studio: chain a Solver and an Explainer agent on a canvas, attach the calculator, Run and watch each agent's turns, tool calls, and tokens stream live with real per-call cost, then Eject the whole flow to a self-contained Python script." />
57
+ </p>
58
+
59
+ <p align="center">
60
+ <em>The studio, end to end: chain a <strong>Solver</strong> and an <strong>Explainer</strong>, attach the calculator, <strong>Run</strong> and watch each agent stream its turns, tool calls, and tokens live — with real per-call cost attribution (here on <code>gpt-5.4-mini</code>, ~$0.0006 for the whole chain) — then <strong>Eject to Python</strong>, a self-contained <code>agent.py</code> with zero <code>bare_agent</code> dependency. The same loop runs local-first on Ollama at $0.</em>
61
+ </p>
62
+
63
+ ## Features
64
+
65
+ | Capability | Detail |
66
+ |---|---|
67
+ | **Framework-free agent loop** | A hand-written tool-use loop over LiteLLM with a 3-axis budget (turns / tokens / wall-clock) + hard cost cap, a retry/fallback ladder, and a self-registering, permission-gated tool registry. The loop is a stateless reducer over an explicit `messages: list[dict]`. |
68
+ | **Local-first, $0 — or BYO frontier key** | Every call goes through LiteLLM, so the model id picks the provider. `ollama_chat/qwen3` runs free and offline; `anthropic/…`, `openai/…`, `gemini/…` are drop-ins. No lock-in. |
69
+ | **Multi-agent chains** | Wire agents agent→agent; the runtime topologically orders them and feeds each answer into the next. Inline runs, queued runs, and ejected code all execute the same chain. |
70
+ | **Visual studio** | A React Flow canvas (Next.js 16 / React 19) to build chains, attach tools, and watch turns / tool calls / tokens stream live over SSE — one readable section per agent. |
71
+ | **Eject to plain Python** | Compile any graph to a standalone `agent.py` (litellm + pydantic only) — tool sources inlined, **zero `bare_agent` import**. Machine-checked to compile. The graph is a convenience, never a cage. |
72
+ | **HITL / permissions** | An `Approver` gates tool calls allow / ask / deny; successful tool output is wrapped `<untrusted_tool_output>` for prompt-injection containment. |
73
+ | **Horizontal scale** | An optional Redis-list job queue + worker pool; Kubernetes + **KEDA scale workers 0→N→0** on queue depth — the same shape as [Argus](https://github.com/subratamondal1/argus)'s searcher fan-out. |
74
+ | **Composition, not configuration** | Seams are Python `Protocol`s — swap the LLM, the approver, or the event sink by passing a different object. No god-object to subclass. |
75
+
76
+ ## Quickstart
77
+
78
+ ```bash
79
+ uv add bare-agent # or: pip install bare-agent
80
+ ```
81
+
82
+ A complete agent in ~30 lines — the docstring becomes the LLM's tool description:
83
+
84
+ ```python
85
+ import asyncio
86
+ from pydantic import BaseModel, Field
87
+ from bare_agent import AgentLoop, Budget, LLMClient, ToolRegistry, get_settings
88
+
89
+ registry = ToolRegistry()
90
+
91
+ class AddArgs(BaseModel):
92
+ a: int = Field(description="first addend")
93
+ b: int = Field(description="second addend")
94
+
95
+ @registry.tool()
96
+ async def add(args: AddArgs) -> int:
97
+ """Add two integers and return their sum."""
98
+ return args.a + args.b
99
+
100
+ async def main() -> None:
101
+ settings = get_settings() # local Ollama by default; set BARE_AGENT_MODEL for frontier
102
+ agent = AgentLoop(
103
+ registry=registry,
104
+ llm=LLMClient.from_settings(settings),
105
+ budget=Budget.from_settings(settings),
106
+ system_prompt="You are a precise assistant. Use tools for arithmetic.",
107
+ )
108
+ result = await agent.run("What is 17 + 25, then add 100 to that?")
109
+ print(result.answer) # -> "142"
110
+ print(result.stop_reason, result.turns, f"${result.cost_usd}") # -> completed 3 $0.0
111
+
112
+ asyncio.run(main())
113
+ ```
114
+
115
+ Run it locally for free:
116
+
117
+ ```bash
118
+ ollama pull qwen3 # one-time (qwen3:30b-a3b-thinking on a 32GB Mac)
119
+ make demo # or: uv run python examples/quickstart.py
120
+ ```
121
+
122
+ ## The studio
123
+
124
+ ```bash
125
+ make web # FastAPI on :8000 + Next.js studio on :3000 → http://localhost:3000/studio
126
+ ```
127
+
128
+ Open `http://localhost:3000/studio`: **Add** agents and wire them into a chain, attach catalog
129
+ tools, pick a model (local qwen3 at $0 or your frontier key), and **Run** — each agent streams its
130
+ turns, tool calls, and tokens live over SSE in its own section. The backend is standalone: `make
131
+ api` runs the control plane alone, and the library works with no UI at all.
132
+
133
+ ## How it works
134
+
135
+ ```
136
+ user input
137
+
138
+
139
+ ┌──────────────┐ answer feeds ┌──────────────┐
140
+ │ Agent 1 │ ───────────────► │ Agent 2 │ ──────────► final answer
141
+ │ + tools │ the next │ + tools │
142
+ └──────────────┘ └──────────────┘
143
+ each agent = ONE hand-written loop:
144
+ explicit messages list · 3-axis budget + cost cap · permission-gated tool dispatch
145
+
146
+ run it: inline over SSE · or queue → worker pool → KEDA scales 0→N→0
147
+ keep it: Eject ──► agent.py (litellm + pydantic only — ZERO bare_agent dependency)
148
+ ```
149
+
150
+ The loop is a **stateless reducer** over an explicit `messages: list[dict]`. That one decision pays
151
+ three ways, all for free:
152
+
153
+ - **Durability** — the list is serializable, so checkpoint it and resume after a crash.
154
+ - **Eject-to-code** — the list *is* the program; there was never a framework underneath to lift out.
155
+ - **Testability** — feed a canned `messages` list (or a fake `CompletionClient`), assert.
156
+
157
+ No metaclass magic, no hidden DAG executor, no god-object to subclass, no state trapped in a
158
+ session. Extensibility is composition: `AgentLoop(llm=..., approver=..., registry=...)`.
159
+
160
+ ### The 8 primitives (each usable on its own — not a god-object)
161
+
162
+ | # | Primitive | Where |
163
+ |---|---|---|
164
+ | ① | Tool registry — `@registry.tool()` → JSON-schema → permission-gated dispatch | `registry.py` |
165
+ | ② | Prompt assembly — the explicit, serializable `messages: list[dict]` | `loop.py` |
166
+ | ③ | Agent loop — `AsyncExitStack` + 3-axis budget + termination + cycle-stop | `loop.py` |
167
+ | ④ | Retry / fallback over LiteLLM (local Ollama **or** any frontier model) | `llm.py` |
168
+ | ⑤ | State / memory — checkpoint the `messages` list (durability for free) | `loop.py` |
169
+ | ⑥ | HITL / permissions — allow / ask / deny, an `Approver` on `ask` | `registry.py` |
170
+ | ⑦ | Observability — `structlog` + an optional `EventSink` (SSE-ready) | `events.py` |
171
+ | ⑧ | Eval gate — golden replay (roadmap) | — |
172
+
173
+ ## Eject
174
+
175
+ Any flow — single agent or a chain — compiles to a standalone script that imports only `litellm`
176
+ and `pydantic`. Tool sources are inlined verbatim; there is **no `bare_agent` import**:
177
+
178
+ ```bash
179
+ uv run --with litellm --with pydantic agent.py "your question"
180
+ ```
181
+
182
+ In the studio, **Eject to Python** shows the generated code and downloads it. The generated file is
183
+ machine-checked to compile. You can read it, diff it, vendor it, and run it after you stop using
184
+ bare-agent entirely — that is the point.
185
+
186
+ ## Configuration
187
+
188
+ Settings are read by [Pydantic Settings](src/bare_agent/config.py) from the environment
189
+ (`BARE_AGENT_` prefix) or `.env` (`cp .env.example .env`). The defaults are fully local and free.
190
+ Common overrides:
191
+
192
+ | Variable | Default | Purpose |
193
+ |---|---|---|
194
+ | `BARE_AGENT_MODEL` | `ollama_chat/qwen3` | LiteLLM model id. Local Ollama by default; `anthropic/…`, `openai/…`, `gemini/…` for hosted. |
195
+ | `BARE_AGENT_OLLAMA_BASE_URL` | `http://localhost:11434` | Ollama server, passed as `api_base` for `ollama_chat/` models. |
196
+ | `BARE_AGENT_FALLBACK_MODELS` | `[]` | Ordered fallback model ids (JSON list) for the retry ladder. |
197
+ | `BARE_AGENT_MAX_TURNS` / `…_TOKENS` / `…_WALLCLOCK_S` / `…_COST_USD` | `8` / `120000` / `180` / `0.50` | The 3-axis budget + hard cost cap; the loop stops on the first to trip. |
198
+ | `BARE_AGENT_USE_QUEUE` | `false` | Route runs through the Redis queue + worker pool (KEDA-autoscalable) instead of inline. |
199
+ | `BARE_AGENT_REDIS_URL` | `redis://localhost:6379/0` | Redis DSN for the run queue + event pub/sub (queue mode). |
200
+
201
+ For a hosted model, set `BARE_AGENT_MODEL=anthropic/…` and export that provider's key
202
+ (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`) — LiteLLM reads it from the environment.
203
+
204
+ ## Development
205
+
206
+ ```bash
207
+ make ci # lock-check + format-check + lint (ruff) + compile + typecheck (ty) + tests (pytest)
208
+ make test # the 29-test suite — hermetic (the LLM and Redis are faked; no daemon needed)
209
+ make web # backend + studio together for local hacking
210
+ make up / down # the Docker stack (api + studio; Ollama stays on the host)
211
+ make queue-up # the Docker stack WITH the KEDA-shaped worker plane (+ redis + worker)
212
+ make help # all targets
213
+ ```
214
+
215
+ Kubernetes manifests live in [`k8s/`](k8s/) — an inline deploy (api + studio) and the KEDA worker
216
+ plane (redis + worker). The studio has its own toolchain ([`apps/studio/AGENTS.md`](apps/studio/AGENTS.md));
217
+ the canonical agent rules for the whole repo are in [`AGENTS.md`](AGENTS.md).
218
+
219
+ <!-- Uncomment once the repo has stars (renders an empty chart at 0):
220
+ ## Star history
221
+
222
+ <p align="center">
223
+ <a href="https://star-history.com/#subratamondal1/bare-agent&Date">
224
+ <img src="https://api.star-history.com/svg?repos=subratamondal1/bare-agent&type=Date" width="600" alt="Star history">
225
+ </a>
226
+ </p>
227
+ -->
228
+
229
+ ## License
230
+
231
+ MIT © 2026 Subrata Mondal — see [LICENSE](LICENSE). Built as the clean, reusable extraction of
232
+ [Argus](https://github.com/subratamondal1/argus)'s agent runtime.
@@ -0,0 +1,43 @@
1
+ """A complete agent in ~30 lines.
2
+
3
+ Run: ollama pull qwen3 && uv run python examples/quickstart.py
4
+ Or: BARE_AGENT_MODEL=anthropic/claude-haiku-4-5 uv run python examples/quickstart.py
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+ from bare_agent import AgentLoop, Budget, LLMClient, ToolRegistry, get_settings
14
+
15
+ registry: ToolRegistry = ToolRegistry()
16
+
17
+
18
+ class AddArgs(BaseModel):
19
+ a: int = Field(description="first addend")
20
+ b: int = Field(description="second addend")
21
+
22
+
23
+ @registry.tool()
24
+ async def add(args: AddArgs) -> int:
25
+ """Add two integers and return their sum."""
26
+ return args.a + args.b
27
+
28
+
29
+ async def main() -> None:
30
+ settings = get_settings()
31
+ agent: AgentLoop = AgentLoop(
32
+ registry=registry,
33
+ llm=LLMClient.from_settings(settings),
34
+ budget=Budget.from_settings(settings),
35
+ system_prompt="You are a precise assistant. Use the add tool for any arithmetic.",
36
+ )
37
+ result = await agent.run("What is 17 + 25, then add 100 to that?")
38
+ print(result.answer)
39
+ print("stop:", result.stop_reason, "| turns:", result.turns, "| cost: $", result.cost_usd)
40
+
41
+
42
+ if __name__ == "__main__":
43
+ asyncio.run(main())
@@ -0,0 +1,73 @@
1
+ [project]
2
+ name = "bare-agent"
3
+ version = "0.0.1"
4
+ description = "A framework-free agent runtime you can read, run, and leave. Own the loop, not the framework. Runs local on Ollama at $0 — or any frontier model."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Subrata Mondal" }]
9
+ keywords = ["agents", "llm", "framework-free", "tool-calling", "ollama", "litellm", "local-first"]
10
+ dependencies = [
11
+ "litellm>=1.55", # the ONLY provider story: local Ollama + every frontier API, one call
12
+ "pydantic>=2.10", # typed tool args + structured (constrained) results
13
+ "pydantic-settings>=2.7", # config via env (NEVER os.getenv)
14
+ "structlog>=24.4", # structured logging (NEVER print)
15
+ "orjson>=3.10", # fast JSON
16
+ "python-dotenv>=1.0", # load .env provider keys into the environment for LiteLLM
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ api = [
21
+ "fastapi>=0.115", # the studio control plane (compile + SSE run)
22
+ "uvicorn[standard]>=0.32", # ASGI server (uvloop + httptools)
23
+ "httpx>=0.28", # the http_get demo tool
24
+ "redis>=5", # the run queue + event pub/sub (queue mode)
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/subratamondal1/bare-agent"
29
+ Repository = "https://github.com/subratamondal1/bare-agent"
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ "ruff>=0.9",
34
+ "ty>=0.0.1a1", # Astral type checker (NEVER mypy/pyright)
35
+ "pytest>=8.3",
36
+ "pytest-asyncio>=0.25",
37
+ "fakeredis>=2", # in-process Redis for the queue-path test (no daemon needed)
38
+ ]
39
+
40
+ [build-system]
41
+ requires = ["hatchling"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/bare_agent"]
46
+
47
+ # Keep the published sdist lean: the library + examples + README + LICENSE only.
48
+ # The studio (apps/), k8s manifests, the demo GIF, and tests live in the repo,
49
+ # not in the package a user pip-installs.
50
+ [tool.hatch.build.targets.sdist]
51
+ include = [
52
+ "/src/bare_agent",
53
+ "/examples",
54
+ "/README.md",
55
+ "/LICENSE",
56
+ ]
57
+
58
+ [tool.ruff]
59
+ line-length = 100
60
+ target-version = "py312"
61
+ src = ["src", "tests", "examples"]
62
+
63
+ [tool.ruff.lint]
64
+ select = ["E", "F", "I", "B", "C4", "UP", "ASYNC", "SIM", "RUF"]
65
+ ignore = ["E501"] # line length handled by formatter, not linter
66
+
67
+ [tool.pytest.ini_options]
68
+ testpaths = ["tests"]
69
+ asyncio_mode = "auto"
70
+ addopts = "-q"
71
+
72
+ [tool.ty.environment]
73
+ python-version = "3.12"