bare-agent 0.2.2 → 0.3.1

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.
package/README.md CHANGED
@@ -4,260 +4,81 @@
4
4
  │ ╠╩╗╠═╣╠╦╝╠╣ ╠═╣║ ╦╠╣ ║║║ ║ │
5
5
  │ ╚═╝╩ ╩╩╚═╚═╝ ╩ ╩╚═╝╚═╝╝╚╝ ╩ │
6
6
  │ think ──→ act ──→ observe │
7
- │ ↑ │ │
7
+ │ ↑ │ │
8
8
  │ └──────────────────┘ │
9
9
  ╰──╮──────────────────────────────╯
10
10
  ╰── the brain, without the bloat
11
-
12
- ```
13
11
 
14
- # bare-agent
12
+ ```
15
13
 
16
14
  **Agent orchestration in ~1700 lines. Zero required deps. MIT license.**
17
15
 
18
- Everything between "call the LLM" and "ship the agent" loop, plan, remember, schedule, checkpoint. Each works alone. All compose together.
16
+ Lightweight enough to understand completely. Complete enough to not reinvent wheels. Not a framework, not 50,000 lines of opinions just composable building blocks for agents.
19
17
 
20
- ```
18
+ ## Quick start
19
+
20
+ ```bash
21
21
  npm install bare-agent
22
22
  ```
23
23
 
24
- ---
25
-
26
- ## Why this exists
27
-
28
- You want to build an agent. You have two choices:
29
-
30
- 1. **Write it from scratch** — 250+ lines of boilerplate. Tool calling loop, retries, provider normalization, memory, state tracking. Everyone reinvents this.
31
- 2. **Adopt a framework** — 50,000 lines, 200 deps, middleware chains, lifecycle hooks, plugin systems. 95% of it is irrelevant to your use case.
32
-
33
- **bare-agent is the middle ground.** Small enough to read in an afternoon. Complete enough that you stop reimplementing the same patterns. Each piece works alone — take what you need, ignore the rest.
34
-
35
- Not a framework. Not an SDK. Just composable building blocks for agents.
36
-
37
- ---
38
-
39
- ## Architecture
40
-
41
- Three layers. You use the first two. You bring the third.
42
-
43
- ### Layer 1: ORCHESTRATION — who does what? in what order? what when things go wrong?
44
-
45
- | Component | What it does | How |
46
- |---|---|---|
47
- | **Planner** | Goal -> step DAG | Structured output prompt, LLM returns JSON dependency graph |
48
- | **State** | Task lifecycle tracking | `pending -> running -> done \| failed`, persisted to JSON file |
49
- | **Stream** | Event streaming | One JSON object per line to stdout, pipe-friendly, any-language |
50
- | **Errors** | Typed error hierarchy | `BareAgentError` base, `ProviderError`, `ToolError`, `TimeoutError`, `CircuitOpenError` |
51
-
52
- ### Layer 2: EXECUTION — how the agent thinks, remembers, acts, and persist?
53
-
54
- | Component | What it does | How |
55
- |---|---|---|
56
- | **Loop** | Think -> act -> observe | Calls OpenAI/Anthropic/Ollama, executes tools, loops until text |
57
- | **Scheduler** | Time-triggered turns | Cron (`0 7 * * 1-5`), relative (`2h`, `30m`), persisted jobs |
58
- | **Memory** | Persist + search | SQLite FTS5 with BM25 (default), JSON file fallback (zero deps) |
59
- | **Checkpoint** | Human approval gate | You provide the transport — readline, Telegram, WebSocket |
60
- | **Retry** | Backoff on failure | Exponential/linear with jitter, retries on 429/5xx/network errors |
61
- | **CircuitBreaker** | Fail-fast on repeated errors | Per-key threshold, auto half-open probe, `wrapProvider()` |
62
- | **Fallback** | Multi-provider resilience | Tries providers in order, AggregateError if all fail |
63
-
64
- ### Layer 3: ACTUATION — you provide this
24
+ **1. Give your AI assistant the integration guide**
65
25
 
66
26
  ```
67
- bare-agent provides the brain. You provide the hands.
68
- Your tools plug into the Loop as functions:
69
-
70
- REST APIs Gmail, Spotify, Calendar, any HTTP endpoint
71
- MCP servers any MCP-compatible tool server
72
- CLI commands termux-api, ffmpeg, git, shell scripts
73
- Browser Playwright, Puppeteer
74
- UI automation ADB, accessibility APIs
27
+ Read bareagent.context.md from node_modules/bare-agent/bareagent.context.md
75
28
  ```
76
29
 
77
- bare-agent does not ship tools. Your tools plug into the Loop as functions `{ name, description, parameters, execute }`. The library handles orchestration. You handle action.
78
-
79
- ### What bare-agent does NOT do
80
-
81
- | Not included | Why | Use instead |
82
- |---|---|---|
83
- | Tool implementations | Actuation is your domain | Your APIs, MCP servers, CLI commands |
84
- | Web UI / dashboard | AG-UI protocol exists | CopilotKit, or build your own |
85
- | Authentication | Every app has different auth | Wrap Checkpoint with your auth |
86
- | Browser automation | Separate concern, too heavy | Playwright, Puppeteer (as a tool) |
87
- | Multi-tenant isolation | Platform problem, not agent problem | Build on top with scope filtering |
88
- | Agent-to-agent protocol | A2A exists for this | Use A2A SDK when needed |
89
-
90
- ---
91
-
92
- ## Quick start
93
-
94
- ### Minimal — 10 lines, one LLM call with tools
95
-
96
- ```javascript
97
- const { Loop } = require('bare-agent');
98
- const { OpenAIProvider } = require('bare-agent/providers');
99
-
100
- const loop = new Loop({
101
- provider: new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY }),
102
- });
30
+ This single file contains component selection, wiring recipes, API signatures, and gotchaseverything an agent needs to use the library correctly.
103
31
 
104
- const result = await loop.run([
105
- { role: 'user', content: 'What is the weather in Berlin?' }
106
- ], [weatherTool]);
32
+ **2. Describe what you want**
107
33
 
108
- console.log(result.text);
109
34
  ```
35
+ I need an agent that:
36
+ - Takes a user goal and breaks it into steps
37
+ - Runs steps in parallel where possible
38
+ - Retries failed steps twice
39
+ - Streams progress as JSONL events
110
40
 
111
- ### With human approval 30 lines
112
-
113
- ```javascript
114
- const { Loop, Checkpoint } = require('bare-agent');
115
- const { AnthropicProvider } = require('bare-agent/providers');
116
-
117
- const checkpoint = new Checkpoint({
118
- tools: ['send_email'],
119
- send: (q) => console.log(`[APPROVE?] ${q}`),
120
- waitForReply: () => new Promise(resolve =>
121
- process.stdin.once('data', d => resolve(d.toString().trim()))
122
- ),
123
- });
124
-
125
- const loop = new Loop({
126
- provider: new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY }),
127
- checkpoint,
128
- });
129
-
130
- const result = await loop.run([
131
- { role: 'user', content: 'Email mom that I will be late' }
132
- ], [emailTool]);
41
+ Use bare-agent. The integration guide is in bareagent.context.md.
133
42
  ```
134
43
 
135
- ### Full autonomous agent40 lines
136
-
137
- ```javascript
138
- const { Loop, Planner, StateMachine, Scheduler,
139
- Memory, Checkpoint, Stream, Retry } = require('bare-agent');
140
- const { AnthropicProvider } = require('bare-agent/providers');
141
- const { SQLiteStore } = require('bare-agent/stores');
142
-
143
- const provider = new AnthropicProvider({
144
- apiKey: process.env.ANTHROPIC_API_KEY,
145
- model: 'claude-haiku-4-5-20251001',
146
- });
147
-
148
- const loop = new Loop({
149
- provider,
150
- planner: new Planner({ provider }),
151
- state: new StateMachine({ file: './tasks.json' }),
152
- memory: new Memory({ store: new SQLiteStore('./agent.db') }),
153
- checkpoint: new Checkpoint({
154
- tools: ['purchase', 'send_email'],
155
- send: (q) => telegram.send(chatId, q),
156
- waitForReply: () => new Promise(r => telegram.once('message', r)),
157
- }),
158
- stream: new Stream({ transport: 'jsonl' }),
159
- retry: new Retry({ maxAttempts: 3, backoff: 'exponential' }),
160
- });
161
-
162
- await loop.runGoal('Book my Berlin trip for next Tuesday');
163
- ```
164
-
165
- ### Resilient multi-provider — circuit breaker + fallback + jitter
166
-
167
- ```javascript
168
- const { Loop, Retry, CircuitBreaker } = require('bare-agent');
169
- const { OpenAI, Anthropic, Fallback } = require('bare-agent/providers');
170
-
171
- const cb = new CircuitBreaker({ threshold: 3, resetAfter: 30000 });
172
-
173
- const provider = new Fallback([
174
- cb.wrapProvider(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }), 'openai'),
175
- cb.wrapProvider(new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }), 'anthropic'),
176
- ]);
177
-
178
- const loop = new Loop({
179
- provider,
180
- retry: new Retry({ maxAttempts: 3, jitter: 'full' }),
181
- });
182
-
183
- const result = await loop.run([
184
- { role: 'user', content: 'Summarize today\'s news' }
185
- ]);
186
- ```
44
+ That's it. The context doc is structured for LLM consumption your agent reads it once and knows how to wire every component.
187
45
 
188
46
  ---
189
47
 
190
- ## LLM Providers
48
+ ## What's inside
191
49
 
192
- All implement one method: `generate(messages, tools, options) -> { text, toolCalls, usage }`.
50
+ Every piece works alone take what you need, ignore the rest.
193
51
 
194
- | Provider | Covers |
52
+ | Component | What it does |
195
53
  |---|---|
196
- | **OpenAI** | OpenAI, OpenRouter, Together, Groq, vLLM, LM Studio any OpenAI-compatible endpoint |
197
- | **Anthropic** | Claude models via native API |
198
- | **Ollama** | Local models, no API key needed |
199
- | **CLIPipe** | Any CLI tool via stdin/stdout (claude, ollama run, etc.) |
200
- | **Fallback** | Tries multiple providers in order transparent to Loop |
201
- | **Bring your own** | Implement `generate()` — one method, full control |
202
-
203
- ## Storage
204
-
205
- | Store | Deps | Search |
206
- |---|---|---|
207
- | **SQLite FTS5** | `better-sqlite3` (peer dep) | Full-text search with BM25 ranking |
208
- | **JSON file** | None | Substring matching |
209
- | **Bring your own** | None | Implement 4 methods for Postgres, Redis, etc. |
210
-
211
- ---
212
-
213
- ## Cross-language usage
214
-
215
- bare-agent runs as a subprocess. Communicate via JSONL on stdin/stdout. Works from any language.
54
+ | **Loop** | Think act → observe → repeat. Calls any LLM, executes your tools, loops until done. Throws on error by default |
55
+ | **Planner** | Break a goal into a step DAG via LLM. Built-in caching (`cacheTTL`) |
56
+ | **runPlan** | Execute steps in parallel waves. Dependency-aware, failure propagation, per-step retry |
57
+ | **Retry** | Exponential/linear backoff with jitter. Respects `err.retryable` |
58
+ | **CircuitBreaker** | Fail fast after N errors. Auto-recovers after cooldown. Per-key isolation |
59
+ | **Fallback** | Try providers in order if one is down, next one picks up. Transparent to Loop |
60
+ | **Memory** | Persist and search context. SQLite with FTS (default) or zero-dep JSON file |
61
+ | **StateMachine** | Task lifecycle tracking with event hooks. `pending → running → done / failed / waiting / cancelled` |
62
+ | **Checkpoint** | Human approval gate. You provide the transport — terminal, Telegram, Slack, whatever |
63
+ | **Scheduler** | Cron (`0 9 * * 1-5`) or relative (`2h`, `30m`). Persisted jobs survive restarts |
64
+ | **Stream** | Structured event emitter. Pipe as JSONL, subscribe in-process, or custom transport |
65
+ | **Errors** | Typed hierarchy `ProviderError`, `ToolError`, `TimeoutError`, `MaxRoundsError`, `CircuitOpenError` |
216
66
 
217
- ```python
218
- import subprocess, json
67
+ **Providers:** OpenAI-compatible (OpenAI, OpenRouter, Groq, vLLM, LM Studio), Anthropic, Ollama, CLIPipe (any CLI tool via stdin/stdout with real-time streaming), Fallback, or bring your own (one method: `generate`). All return the same shape — swap freely.
219
68
 
220
- proc = subprocess.Popen(
221
- ['npx', 'bare-agent', '--jsonl'],
222
- stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True
223
- )
69
+ **Tools:** Any function is a tool. REST APIs, MCP servers, CLI commands, browser automation, shell scripts — if it's a function, it works.
224
70
 
225
- proc.stdin.write(json.dumps({
226
- "method": "run",
227
- "params": {"goal": "What is 2+2?"}
228
- }) + '\n')
229
- proc.stdin.flush()
71
+ **Cross-language:** Runs as a subprocess. Communicate via JSONL on stdin/stdout from Python, Go, Rust, or anything that can spawn a process.
230
72
 
231
- for line in proc.stdout:
232
- event = json.loads(line)
233
- if event['type'] == 'loop:done':
234
- print(event['data']['text'])
235
- break
236
- ```
237
-
238
- Same pattern works from Go, Rust, Java, Ruby — any language that can spawn a process and read lines.
73
+ **Deps:** 0 required. Optional: `cron-parser` (cron expressions), `better-sqlite3` (SQLite store).
239
74
 
240
75
  ---
241
76
 
242
- ## Dependencies
243
-
244
- ```
245
- required: 0
246
- optional: cron-parser (for cron expressions in scheduler)
247
- peer: better-sqlite3 (for SQLite memory store)
248
- total lines: ~1700
249
- ```
250
-
251
- ## Status
252
-
253
- **Production-validated.** bare-agent powers the SOAR2 pipeline in [Aurora](https://github.com/hamr0/aurora), replacing ~400 lines of hand-rolled agent orchestration with ~60 lines of bare-agent wiring. In production use, bare-agent eliminated:
77
+ ## Production-validated
254
78
 
255
- - **Boilerplate** Tool-calling loop, provider normalization, retry logic, and state tracking that every agent project reinvents. Aurora's SOAR2 pipeline dropped from custom loop + manual state management to `Loop + Planner + runPlan + StateMachine`.
256
- - **Fragile glue code** — Manual wave execution, dependency resolution, and error propagation replaced by `runPlan` with built-in parallelism and failure cascading.
257
- - **Provider lock-in** — Switching from OpenAI to Anthropic to CLIPipe required zero orchestration changes — just swap the provider constructor.
258
- - **Debugging friction** — Structured `[ComponentName]` error prefixes and `Stream` events made failures traceable in minutes instead of hours.
79
+ bare-agent powers the SOAR2 pipeline in [Aurora](https://github.com/hamr0/aurora), replacing ~400 lines of hand-rolled orchestration with ~60 lines of bare-agent wiring zero workarounds, zero framework plumbing, 100% domain logic.
259
80
 
260
- See [project plan](docs/01-product/prd.md) for the full design. See [CHANGELOG.md](CHANGELOG.md) for release history.
81
+ For wiring recipes and API details, see the **[Integration Guide](bareagent.context.md)** (LLM-optimized). For the full human guide — usage patterns, composition examples, and what bare-agent deliberately doesn't build in (with recipes to do it yourself), see the **[Usage Guide](docs/02-features/usage-guide.md)**. For error reference, see **[Error Guide](docs/02-features/errors.md)**. For release history, see **[CHANGELOG](CHANGELOG.md)**.
261
82
 
262
83
  ## License
263
84
 
package/index.js CHANGED
@@ -17,6 +17,7 @@ const {
17
17
  TimeoutError,
18
18
  ValidationError,
19
19
  CircuitOpenError,
20
+ MaxRoundsError,
20
21
  } = require('./src/errors');
21
22
 
22
23
  module.exports = {
@@ -36,4 +37,5 @@ module.exports = {
36
37
  TimeoutError,
37
38
  ValidationError,
38
39
  CircuitOpenError,
40
+ MaxRoundsError,
39
41
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-agent",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "files": [
5
5
  "index.js",
6
6
  "src/",
@@ -39,7 +39,7 @@
39
39
  "cron-parser": "^4.9.0"
40
40
  },
41
41
  "peerDependencies": {
42
- "better-sqlite3": "^12.6.2"
42
+ "better-sqlite3": ">=9.0.0"
43
43
  },
44
44
  "peerDependenciesMeta": {
45
45
  "better-sqlite3": {
package/src/errors.js CHANGED
@@ -43,6 +43,12 @@ class CircuitOpenError extends BareAgentError {
43
43
  }
44
44
  }
45
45
 
46
+ class MaxRoundsError extends BareAgentError {
47
+ constructor(message, opts = {}) {
48
+ super(message || 'Loop exceeded maximum rounds', { code: 'MAX_ROUNDS', retryable: false, ...opts });
49
+ }
50
+ }
51
+
46
52
  module.exports = {
47
53
  BareAgentError,
48
54
  ProviderError,
@@ -50,4 +56,5 @@ module.exports = {
50
56
  TimeoutError,
51
57
  ValidationError,
52
58
  CircuitOpenError,
59
+ MaxRoundsError,
53
60
  };
package/src/loop.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { ToolError } = require('./errors');
3
+ const { ToolError, MaxRoundsError } = require('./errors');
4
4
 
5
5
  class Loop {
6
6
  /**
@@ -25,6 +25,7 @@ class Loop {
25
25
  this.onToolCall = options.onToolCall || null;
26
26
  this.onText = options.onText || null;
27
27
  this.onError = options.onError || null;
28
+ this.throwOnError = options.throwOnError !== undefined ? options.throwOnError : true;
28
29
  this.store = options.store || null;
29
30
  this._stopped = false;
30
31
  this._history = []; // for chat() stateful mode
@@ -78,6 +79,7 @@ class Loop {
78
79
  } catch (err) {
79
80
  this.stream?.emit({ type: 'loop:error', data: { error: err.message, round } });
80
81
  this.onError?.(err);
82
+ if (this.throwOnError) throw err;
81
83
  return { text: '', toolCalls: [], usage: lastUsage, error: err.message };
82
84
  }
83
85
 
@@ -148,6 +150,7 @@ class Loop {
148
150
  // maxRounds exceeded
149
151
  const warning = `[Loop] ended after ${this.maxRounds} rounds without final response`;
150
152
  this.stream?.emit({ type: 'loop:done', data: { text: '', warning } });
153
+ if (this.throwOnError) throw new MaxRoundsError(warning);
151
154
  return { text: '', toolCalls: [], usage: lastUsage, error: warning };
152
155
  }
153
156
 
package/src/planner.js CHANGED
@@ -25,6 +25,8 @@ class Planner {
25
25
  if (!options.provider) throw new Error('[Planner] requires a provider');
26
26
  this.provider = options.provider;
27
27
  this.prompt = options.prompt || PLAN_PROMPT;
28
+ this._cacheTTL = options.cacheTTL || 0;
29
+ this._cache = new Map();
28
30
  }
29
31
 
30
32
  /**
@@ -37,6 +39,14 @@ class Planner {
37
39
  * @throws {Error} `[Planner] step missing id or action` — when a step lacks required fields.
38
40
  */
39
41
  async plan(goal, context = {}) {
42
+ if (this._cacheTTL > 0) {
43
+ const cacheKey = goal + '|' + (context.info || '');
44
+ const cached = this._cache.get(cacheKey);
45
+ if (cached && Date.now() < cached.expiresAt) {
46
+ return cached.result;
47
+ }
48
+ }
49
+
40
50
  const messages = [
41
51
  { role: 'system', content: this.prompt },
42
52
  ];
@@ -50,7 +60,18 @@ class Planner {
50
60
  temperature: 0,
51
61
  });
52
62
 
53
- return this._parse(result.text);
63
+ const steps = this._parse(result.text);
64
+
65
+ if (this._cacheTTL > 0) {
66
+ const cacheKey = goal + '|' + (context.info || '');
67
+ this._cache.set(cacheKey, { result: steps, expiresAt: Date.now() + this._cacheTTL });
68
+ }
69
+
70
+ return steps;
71
+ }
72
+
73
+ clearCache() {
74
+ this._cache.clear();
54
75
  }
55
76
 
56
77
  _parse(text) {
@@ -23,6 +23,7 @@ class CLIPipeProvider {
23
23
  this.env = options.env || undefined;
24
24
  this.timeout = options.timeout ?? 30000;
25
25
  this.systemPromptFlag = options.systemPromptFlag || null;
26
+ this.onChunk = options.onChunk || null;
26
27
  }
27
28
 
28
29
  /**
@@ -88,7 +89,7 @@ class CLIPipeProvider {
88
89
  let stderr = '';
89
90
  let killed = false;
90
91
 
91
- child.stdout.on('data', d => { stdout += d; });
92
+ child.stdout.on('data', d => { stdout += d; this.onChunk?.(d.toString()); });
92
93
  child.stderr.on('data', d => { stderr += d; });
93
94
 
94
95
  child.on('error', err => {