bare-agent 0.1.0 → 0.1.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 ADDED
@@ -0,0 +1,229 @@
1
+ ```
2
+ ╭─────────────────────────────────╮
3
+ │ ╔╗ ╔═╗╦═╗╔═╗ ╔═╗╔═╗╔═╗╔╗╔╔╦╗ │
4
+ │ ╠╩╗╠═╣╠╦╝╠╣ ╠═╣║ ╦╠╣ ║║║ ║ │
5
+ │ ╚═╝╩ ╩╩╚═╚═╝ ╩ ╩╚═╝╚═╝╝╚╝ ╩ │
6
+ │ think ──→ act ──→ observe │
7
+ │ ↑ │ │
8
+ │ └──────────────────┘ │
9
+ ╰──╮──────────────────────────────╯
10
+ ╰── the brain, without the bloat
11
+
12
+ ```
13
+
14
+ # bare-agent
15
+
16
+ **Agent orchestration in ~800 lines. Zero required deps. MIT license.**
17
+
18
+ Everything between "call the LLM" and "ship the agent" — loop, plan, remember, schedule, checkpoint. Each works alone. All compose together.
19
+
20
+ ```
21
+ npm install bare-agent
22
+ ```
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
+
51
+ ### Layer 2: EXECUTION — how the agent thinks, remembers, acts, and persist?
52
+
53
+ | Component | What it does | How |
54
+ |---|---|---|
55
+ | **Loop** | Think -> act -> observe | Calls OpenAI/Anthropic/Ollama, executes tools, loops until text |
56
+ | **Scheduler** | Time-triggered turns | Cron (`0 7 * * 1-5`), relative (`2h`, `30m`), persisted jobs |
57
+ | **Memory** | Persist + search | SQLite FTS5 with BM25 (default), JSON file fallback (zero deps) |
58
+ | **Checkpoint** | Human approval gate | You provide the transport — readline, Telegram, WebSocket |
59
+ | **Retry** | Backoff on failure | Exponential/linear, retries on 429/5xx/network errors |
60
+
61
+ ### Layer 3: ACTUATION — you provide this
62
+
63
+ ```
64
+ bare-agent provides the brain. You provide the hands.
65
+ Your tools plug into the Loop as functions:
66
+
67
+ REST APIs Gmail, Spotify, Calendar, any HTTP endpoint
68
+ MCP servers any MCP-compatible tool server
69
+ CLI commands termux-api, ffmpeg, git, shell scripts
70
+ Browser Playwright, Puppeteer
71
+ UI automation ADB, accessibility APIs
72
+ ```
73
+
74
+ 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.
75
+
76
+ ### What bare-agent does NOT do
77
+
78
+ | Not included | Why | Use instead |
79
+ |---|---|---|
80
+ | Tool implementations | Actuation is your domain | Your APIs, MCP servers, CLI commands |
81
+ | Web UI / dashboard | AG-UI protocol exists | CopilotKit, or build your own |
82
+ | Authentication | Every app has different auth | Wrap Checkpoint with your auth |
83
+ | Browser automation | Separate concern, too heavy | Playwright, Puppeteer (as a tool) |
84
+ | Multi-tenant isolation | Platform problem, not agent problem | Build on top with scope filtering |
85
+ | Agent-to-agent protocol | A2A exists for this | Use A2A SDK when needed |
86
+
87
+ ---
88
+
89
+ ## Quick start
90
+
91
+ ### Minimal — 10 lines, one LLM call with tools
92
+
93
+ ```javascript
94
+ const { Loop } = require('bare-agent');
95
+ const { OpenAIProvider } = require('bare-agent/providers');
96
+
97
+ const loop = new Loop({
98
+ provider: new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY }),
99
+ });
100
+
101
+ const result = await loop.run([
102
+ { role: 'user', content: 'What is the weather in Berlin?' }
103
+ ], [weatherTool]);
104
+
105
+ console.log(result.text);
106
+ ```
107
+
108
+ ### With human approval — 30 lines
109
+
110
+ ```javascript
111
+ const { Loop, Checkpoint } = require('bare-agent');
112
+ const { AnthropicProvider } = require('bare-agent/providers');
113
+
114
+ const checkpoint = new Checkpoint({
115
+ tools: ['send_email'],
116
+ send: (q) => console.log(`[APPROVE?] ${q}`),
117
+ waitForReply: () => new Promise(resolve =>
118
+ process.stdin.once('data', d => resolve(d.toString().trim()))
119
+ ),
120
+ });
121
+
122
+ const loop = new Loop({
123
+ provider: new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY }),
124
+ checkpoint,
125
+ });
126
+
127
+ const result = await loop.run([
128
+ { role: 'user', content: 'Email mom that I will be late' }
129
+ ], [emailTool]);
130
+ ```
131
+
132
+ ### Full autonomous agent — 40 lines
133
+
134
+ ```javascript
135
+ const { Loop, Planner, StateMachine, Scheduler,
136
+ Memory, Checkpoint, Stream, Retry } = require('bare-agent');
137
+ const { AnthropicProvider } = require('bare-agent/providers');
138
+ const { SQLiteStore } = require('bare-agent/stores');
139
+
140
+ const provider = new AnthropicProvider({
141
+ apiKey: process.env.ANTHROPIC_API_KEY,
142
+ model: 'claude-haiku-4-5-20251001',
143
+ });
144
+
145
+ const loop = new Loop({
146
+ provider,
147
+ planner: new Planner({ provider }),
148
+ state: new StateMachine({ file: './tasks.json' }),
149
+ memory: new Memory({ store: new SQLiteStore('./agent.db') }),
150
+ checkpoint: new Checkpoint({
151
+ tools: ['purchase', 'send_email'],
152
+ send: (q) => telegram.send(chatId, q),
153
+ waitForReply: () => new Promise(r => telegram.once('message', r)),
154
+ }),
155
+ stream: new Stream({ transport: 'jsonl' }),
156
+ retry: new Retry({ maxAttempts: 3, backoff: 'exponential' }),
157
+ });
158
+
159
+ await loop.runGoal('Book my Berlin trip for next Tuesday');
160
+ ```
161
+
162
+ ---
163
+
164
+ ## LLM Providers
165
+
166
+ Three built-in. All implement one method: `generate(messages, tools, options) -> { text, toolCalls, usage }`.
167
+
168
+ | Provider | Covers |
169
+ |---|---|
170
+ | **OpenAI** | OpenAI, OpenRouter, Together, Groq, vLLM, LM Studio — any OpenAI-compatible endpoint |
171
+ | **Anthropic** | Claude models via native API |
172
+ | **Ollama** | Local models, no API key needed |
173
+ | **Bring your own** | Implement `generate()` — one method, full control |
174
+
175
+ ## Storage
176
+
177
+ | Store | Deps | Search |
178
+ |---|---|---|
179
+ | **SQLite FTS5** | `better-sqlite3` (peer dep) | Full-text search with BM25 ranking |
180
+ | **JSON file** | None | Substring matching |
181
+ | **Bring your own** | None | Implement 4 methods for Postgres, Redis, etc. |
182
+
183
+ ---
184
+
185
+ ## Cross-language usage
186
+
187
+ bare-agent runs as a subprocess. Communicate via JSONL on stdin/stdout. Works from any language.
188
+
189
+ ```python
190
+ import subprocess, json
191
+
192
+ proc = subprocess.Popen(
193
+ ['npx', 'bare-agent', '--jsonl'],
194
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True
195
+ )
196
+
197
+ proc.stdin.write(json.dumps({
198
+ "method": "run",
199
+ "params": {"goal": "What is 2+2?"}
200
+ }) + '\n')
201
+ proc.stdin.flush()
202
+
203
+ for line in proc.stdout:
204
+ event = json.loads(line)
205
+ if event['type'] == 'loop:done':
206
+ print(event['data']['text'])
207
+ break
208
+ ```
209
+
210
+ Same pattern works from Go, Rust, Java, Ruby — any language that can spawn a process and read lines.
211
+
212
+ ---
213
+
214
+ ## Dependencies
215
+
216
+ ```
217
+ required: 0
218
+ optional: cron-parser (for cron expressions in scheduler)
219
+ peer: better-sqlite3 (for SQLite memory store)
220
+ total lines: ~820
221
+ ```
222
+
223
+ ## Status
224
+
225
+ Early development. Core components built and validated through POCs. See [project plan](docs/01-product/prd.md) for the full design.
226
+
227
+ ## License
228
+
229
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { createInterface } = require('node:readline');
5
+ const { Loop } = require('../src/loop');
6
+ const { Stream } = require('../src/stream');
7
+ const { JsonlTransport } = require('../src/transport-jsonl');
8
+
9
+ const args = process.argv.slice(2);
10
+ const flag = (name) => {
11
+ const i = args.indexOf(`--${name}`);
12
+ return i >= 0 ? args[i + 1] : undefined;
13
+ };
14
+
15
+ const providerName = flag('provider') || 'openai';
16
+ const model = flag('model');
17
+
18
+ function createProvider() {
19
+ if (providerName === 'openai') {
20
+ const { OpenAIProvider } = require('../src/provider-openai');
21
+ return new OpenAIProvider({
22
+ apiKey: process.env.OPENAI_API_KEY,
23
+ ...(model && { model }),
24
+ });
25
+ }
26
+ if (providerName === 'anthropic') {
27
+ const { AnthropicProvider } = require('../src/provider-anthropic');
28
+ return new AnthropicProvider({
29
+ apiKey: process.env.ANTHROPIC_API_KEY,
30
+ ...(model && { model }),
31
+ });
32
+ }
33
+ if (providerName === 'ollama') {
34
+ const { OllamaProvider } = require('../src/provider-ollama');
35
+ return new OllamaProvider({
36
+ ...(model && { model }),
37
+ ...(flag('url') && { url: flag('url') }),
38
+ });
39
+ }
40
+ process.stderr.write(`Unknown provider: ${providerName}\n`);
41
+ process.exit(1);
42
+ }
43
+
44
+ const stream = new Stream({ transport: new JsonlTransport() });
45
+ const loop = new Loop({ provider: createProvider(), stream });
46
+
47
+ let pending = 0;
48
+ let closing = false;
49
+
50
+ const rl = createInterface({ input: process.stdin });
51
+ rl.on('line', async (line) => {
52
+ pending++;
53
+ try {
54
+ const req = JSON.parse(line);
55
+ const messages = req.params?.messages || [
56
+ { role: 'user', content: req.params?.goal || '' },
57
+ ];
58
+ const result = await loop.run(messages, []);
59
+ stream.emit({ type: 'result', data: result });
60
+ } catch (err) {
61
+ stream.emit({ type: 'error', data: { error: err.message } });
62
+ } finally {
63
+ pending--;
64
+ if (closing && pending === 0) process.exit(0);
65
+ }
66
+ });
67
+
68
+ rl.on('close', () => {
69
+ closing = true;
70
+ if (pending === 0) process.exit(0);
71
+ });
package/index.js CHANGED
@@ -1 +1,21 @@
1
- // bare-agent — lightweight agent orchestration
1
+ 'use strict';
2
+
3
+ const { Loop } = require('./src/loop');
4
+ const { Planner } = require('./src/planner');
5
+ const { StateMachine } = require('./src/state');
6
+ const { Scheduler } = require('./src/scheduler');
7
+ const { Checkpoint } = require('./src/checkpoint');
8
+ const { Memory } = require('./src/memory');
9
+ const { Stream } = require('./src/stream');
10
+ const { Retry } = require('./src/retry');
11
+
12
+ module.exports = {
13
+ Loop,
14
+ Planner,
15
+ StateMachine,
16
+ Scheduler,
17
+ Checkpoint,
18
+ Memory,
19
+ Stream,
20
+ Retry,
21
+ };
package/package.json CHANGED
@@ -1,16 +1,51 @@
1
1
  {
2
2
  "name": "bare-agent",
3
- "version": "0.1.0",
4
- "description": "Lightweight, composable agent orchestration. ~800 lines, 0 required deps. Use what you need, ignore the rest.",
3
+ "version": "0.1.1",
4
+ "files": [
5
+ "index.js",
6
+ "src/",
7
+ "bin/"
8
+ ],
9
+ "description": "Lightweight, composable agent orchestration. ~800 lines, 0 required deps.",
5
10
  "license": "MIT",
6
- "author": "amrhas82",
7
- "keywords": ["agent", "llm", "orchestration", "ai", "tool-calling", "planner", "lightweight"],
11
+ "author": "hamr0",
8
12
  "repository": {
9
13
  "type": "git",
10
- "url": "https://github.com/amrhas82/bare-agent"
14
+ "url": "git+https://github.com/hamr0/bareagent.git"
11
15
  },
12
16
  "main": "index.js",
17
+ "bin": {
18
+ "bare-agent": "./bin/cli.js"
19
+ },
20
+ "exports": {
21
+ ".": "./index.js",
22
+ "./providers": "./src/providers.js",
23
+ "./stores": "./src/stores.js"
24
+ },
13
25
  "engines": {
14
26
  "node": ">=18"
27
+ },
28
+ "keywords": [
29
+ "agent",
30
+ "llm",
31
+ "orchestration",
32
+ "ai",
33
+ "tool-calling",
34
+ "planner",
35
+ "lightweight"
36
+ ],
37
+ "optionalDependencies": {
38
+ "cron-parser": "^4.9.0"
39
+ },
40
+ "peerDependencies": {
41
+ "better-sqlite3": "^12.6.2"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "better-sqlite3": {
45
+ "optional": true
46
+ }
47
+ },
48
+ "scripts": {
49
+ "test": "node --test test/**/*.test.js"
15
50
  }
16
51
  }
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ class Checkpoint {
4
+ constructor(options = {}) {
5
+ this.tools = new Set(options.tools || []);
6
+ this.send = options.send || null;
7
+ this.waitForReply = options.waitForReply || null;
8
+ this.shouldAskFn = options.shouldAsk || null; // custom predicate override
9
+ }
10
+
11
+ shouldAsk(toolName, args) {
12
+ if (this.shouldAskFn) return this.shouldAskFn(toolName, args);
13
+ return this.tools.has(toolName);
14
+ }
15
+
16
+ /**
17
+ * Send a question and wait for a reply.
18
+ * @param {string} question - The approval question to send.
19
+ * @param {object} [context={}] - Context passed to send and waitForReply.
20
+ * @returns {Promise<string|null>} The user's reply, or null.
21
+ * @throws {Error} `[Checkpoint] send and waitForReply callbacks required` — when callbacks are missing.
22
+ */
23
+ async ask(question, context = {}) {
24
+ if (!this.send || !this.waitForReply) {
25
+ throw new Error('[Checkpoint] send and waitForReply callbacks required');
26
+ }
27
+ await this.send(question, context);
28
+ const reply = await this.waitForReply(context);
29
+ return reply ?? null;
30
+ }
31
+ }
32
+
33
+ module.exports = { Checkpoint };
package/src/loop.js ADDED
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+
3
+ class Loop {
4
+ /**
5
+ * @param {object} options
6
+ * @param {object} options.provider - LLM provider (must implement generate()).
7
+ * @param {number} [options.maxRounds=5] - Maximum think/act/observe cycles.
8
+ * @param {string} [options.system] - System prompt prepended to messages.
9
+ * @param {object} [options.checkpoint] - Checkpoint instance for human-in-the-loop.
10
+ * @param {object} [options.retry] - Retry instance for backoff on failures.
11
+ * @param {object} [options.stream] - Stream instance for event emission.
12
+ * @throws {Error} `[Loop] requires a provider` — when options.provider is missing.
13
+ */
14
+ constructor(options = {}) {
15
+ if (!options.provider) throw new Error('[Loop] requires a provider');
16
+ this.provider = options.provider;
17
+ this.maxRounds = options.maxRounds || 5;
18
+ this.system = options.system || null;
19
+ this.checkpoint = options.checkpoint || null;
20
+ this.retry = options.retry || null;
21
+ this.stream = options.stream || null;
22
+ this.onToolCall = options.onToolCall || null;
23
+ this.onText = options.onText || null;
24
+ this.onError = options.onError || null;
25
+ this._stopped = false;
26
+ this._history = []; // for chat() stateful mode
27
+ }
28
+
29
+ /**
30
+ * Run the think/act/observe loop.
31
+ * @param {Array<object>} messages - Conversation messages in OpenAI format.
32
+ * @param {Array<object>} [tools=[]] - Tool definitions with name, execute, description, parameters.
33
+ * @param {object} [options={}] - Per-run overrides (system, temperature, etc.).
34
+ * @returns {Promise<{text: string, toolCalls: Array, usage: object, error: string|null}>}
35
+ * @throws {Error} `[Loop] Tool is missing a name` — when a tool has no name or a non-string name.
36
+ * @throws {Error} `[Loop] Tool "X" is missing an execute() function` — when execute is not a function.
37
+ * @throws {Error} `[Loop] Tool "X" has invalid parameters` — when parameters is not an object.
38
+ */
39
+ async run(messages, tools = [], options = {}) {
40
+ this._stopped = false;
41
+ const system = options.system || this.system;
42
+ const msgs = system
43
+ ? [{ role: 'system', content: system }, ...messages]
44
+ : [...messages];
45
+ const toolMap = new Map(tools.map(t => [t.name, t]));
46
+
47
+ // Validate tools at wire time
48
+ for (const tool of tools) {
49
+ if (typeof tool.name !== 'string' || !tool.name) {
50
+ throw new Error(`[Loop] Tool is missing a name (got ${JSON.stringify(tool.name)}). Every tool must have a non-empty string name.`);
51
+ }
52
+ if (typeof tool.execute !== 'function') {
53
+ throw new Error(`[Loop] Tool "${tool.name}" is missing an execute() function.`);
54
+ }
55
+ if (tool.description !== undefined && typeof tool.description !== 'string') {
56
+ console.warn(`[Loop] Tool "${tool.name}" has a non-string description — providers may ignore it.`);
57
+ }
58
+ if (tool.parameters !== undefined && (typeof tool.parameters !== 'object' || tool.parameters === null)) {
59
+ throw new Error(`[Loop] Tool "${tool.name}" has invalid parameters — expected an object, got ${typeof tool.parameters}.`);
60
+ }
61
+ }
62
+
63
+ this.stream?.emit({ type: 'loop:start', data: { messageCount: msgs.length } });
64
+
65
+ let lastUsage = { inputTokens: 0, outputTokens: 0 };
66
+
67
+ for (let round = 0; round < this.maxRounds; round++) {
68
+ if (this._stopped) break;
69
+
70
+ let result;
71
+ try {
72
+ const generate = () => this.provider.generate(msgs, tools, options);
73
+ result = this.retry ? await this.retry.call(generate) : await generate();
74
+ } catch (err) {
75
+ this.stream?.emit({ type: 'loop:error', data: { error: err.message, round } });
76
+ this.onError?.(err);
77
+ return { text: '', toolCalls: [], usage: lastUsage, error: err.message };
78
+ }
79
+
80
+ lastUsage = result.usage || lastUsage;
81
+
82
+ // No tool calls — LLM gave a final text response
83
+ if (!result.toolCalls || result.toolCalls.length === 0) {
84
+ this.stream?.emit({ type: 'loop:text', data: { text: result.text } });
85
+ this.onText?.(result.text);
86
+ this.stream?.emit({ type: 'loop:done', data: { text: result.text, usage: lastUsage } });
87
+ return { text: result.text, toolCalls: [], usage: lastUsage, error: null };
88
+ }
89
+
90
+ // Execute tool calls
91
+ msgs.push({
92
+ role: 'assistant',
93
+ content: result.text || null,
94
+ tool_calls: result.toolCalls.map(tc => ({
95
+ id: tc.id,
96
+ type: 'function',
97
+ function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
98
+ })),
99
+ });
100
+
101
+ for (const tc of result.toolCalls) {
102
+ if (this._stopped) break;
103
+
104
+ const tool = toolMap.get(tc.name);
105
+ if (!tool) {
106
+ const errMsg = `[Loop] Unknown tool: ${tc.name}`;
107
+ msgs.push({ role: 'tool', tool_call_id: tc.id, content: errMsg });
108
+ this.stream?.emit({ type: 'loop:tool_result', data: { tool: tc.name, error: errMsg } });
109
+ continue;
110
+ }
111
+
112
+ // Checkpoint — ask for approval before executing
113
+ if (this.checkpoint?.shouldAsk(tc.name, tc.arguments)) {
114
+ this.stream?.emit({ type: 'checkpoint:ask', data: { tool: tc.name, args: tc.arguments } });
115
+ const reply = await this.checkpoint.ask(
116
+ `Approve ${tc.name}(${JSON.stringify(tc.arguments)})?`,
117
+ { tool: tc.name, args: tc.arguments }
118
+ );
119
+ this.stream?.emit({ type: 'checkpoint:reply', data: { reply } });
120
+ if (!reply || reply.toLowerCase() === 'no' || reply.toLowerCase() === 'n') {
121
+ msgs.push({ role: 'tool', tool_call_id: tc.id, content: 'User denied this action.' });
122
+ continue;
123
+ }
124
+ }
125
+
126
+ this.stream?.emit({ type: 'loop:tool_call', data: { tool: tc.name, args: tc.arguments } });
127
+ this.onToolCall?.(tc.name, tc.arguments);
128
+
129
+ try {
130
+ const execute = () => tool.execute(tc.arguments);
131
+ const toolResult = this.retry ? await this.retry.call(execute) : await execute();
132
+ const content = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult);
133
+ msgs.push({ role: 'tool', tool_call_id: tc.id, content });
134
+ this.stream?.emit({ type: 'loop:tool_result', data: { tool: tc.name, result: content } });
135
+ } catch (err) {
136
+ const errMsg = `[Loop] Tool error: ${err.message}`;
137
+ msgs.push({ role: 'tool', tool_call_id: tc.id, content: errMsg });
138
+ this.stream?.emit({ type: 'loop:tool_result', data: { tool: tc.name, error: errMsg } });
139
+ }
140
+ }
141
+ }
142
+
143
+ // maxRounds exceeded
144
+ const warning = `[Loop] ended after ${this.maxRounds} rounds without final response`;
145
+ this.stream?.emit({ type: 'loop:done', data: { text: '', warning } });
146
+ return { text: '', toolCalls: [], usage: lastUsage, error: warning };
147
+ }
148
+
149
+ async chat(text, tools = [], options = {}) {
150
+ this._history.push({ role: 'user', content: text });
151
+ const result = await this.run(this._history, tools, options);
152
+ if (result.text) {
153
+ this._history.push({ role: 'assistant', content: result.text });
154
+ }
155
+ return result;
156
+ }
157
+
158
+ stop() {
159
+ this._stopped = true;
160
+ }
161
+ }
162
+
163
+ module.exports = { Loop };
package/src/memory.js ADDED
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Persistence + search across turns and sessions.
5
+ * Thin wrapper that delegates to a swappable store.
6
+ *
7
+ * Interface:
8
+ * store(content, metadata) → id
9
+ * search(query, options) → [{ id, content, metadata, score }]
10
+ * get(id) → { content, metadata }
11
+ * delete(id) → void
12
+ *
13
+ * Stores (swappable):
14
+ * SQLite FTS5 — store-sqlite.js (peer dep: better-sqlite3)
15
+ * JSON file — store-jsonfile.js (zero deps)
16
+ * Bring your own: implement { store, search, get, delete }
17
+ */
18
+ class Memory {
19
+ /**
20
+ * @param {object} options
21
+ * @param {object} options.store - Store backend (must implement store/search/get/delete).
22
+ * @throws {Error} `[Memory] requires options.store` — when options.store is missing.
23
+ */
24
+ constructor(options = {}) {
25
+ if (!options.store) throw new Error('[Memory] requires options.store');
26
+ this._store = options.store;
27
+ }
28
+
29
+ store(content, metadata = {}) {
30
+ return this._store.store(content, metadata);
31
+ }
32
+
33
+ search(query, options = {}) {
34
+ return this._store.search(query, options);
35
+ }
36
+
37
+ get(id) {
38
+ return this._store.get(id);
39
+ }
40
+
41
+ delete(id) {
42
+ return this._store.delete(id);
43
+ }
44
+ }
45
+
46
+ module.exports = { Memory };