clawlet 0.4.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawlet",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "A lightweight AI based personal assistant.",
5
5
  "main": "src/cli.ts",
6
6
  "type": "module",
@@ -94,8 +94,15 @@ const runTestCaseFile = async (filename: string) => {
94
94
  onError: (e) => { throw e; }
95
95
  });
96
96
 
97
- (agent as any).inputQueue.push({ text: data.input, label: 'test' });
98
- await (agent as any).processQueue();
97
+ await (new Promise((resolve, reject) => {
98
+ agent.addInput({
99
+ onMessage: async (handler : (text: string, label: string) => Promise<void>) => {
100
+ await handler(data.input, 'test');
101
+ resolve(true);
102
+ },
103
+ start: () => {}
104
+ })
105
+ }));
99
106
 
100
107
  // 3. ASSERTIONS
101
108
 
package/src/agent.ts CHANGED
@@ -24,7 +24,7 @@ const GENERATE_TEXT_MAX_STEPS = 30;
24
24
  // --- ADAPTER INTERFACES ---
25
25
 
26
26
  export interface InputAdapter {
27
- onMessage(handler: (text: string, label: string) => void): void;
27
+ onMessage(handler: (text: string, label: string) => Promise<void>): void;
28
28
  start(): void;
29
29
  }
30
30
 
@@ -194,7 +194,6 @@ export class Agent {
194
194
  private tools: ReturnType<typeof createTools>;
195
195
  private inputAdapters: InputAdapter[] = [];
196
196
  private outputAdapters: OutputAdapter[] = [];
197
- private inputQueue: { text: string; label: string }[] = [];
198
197
  private processing = false;
199
198
  private initialized = false;
200
199
  private bootstrapPrompt: string | null = null;
@@ -207,9 +206,9 @@ export class Agent {
207
206
 
208
207
  addInput(adapter: InputAdapter): this {
209
208
  this.inputAdapters.push(adapter);
210
- adapter.onMessage((text, label) => {
211
- this.inputQueue.push({ text, label });
212
- this.processQueue();
209
+ adapter.onMessage(async (text, label) => {
210
+ await this.memory.queue.push("main-session", { text, label });
211
+ await this.processQueue();
213
212
  });
214
213
  return this;
215
214
  }
@@ -224,6 +223,7 @@ export class Agent {
224
223
  for (const adapter of this.inputAdapters) {
225
224
  adapter.start();
226
225
  }
226
+ await this.processQueue();
227
227
  }
228
228
 
229
229
  private async init() {
@@ -284,86 +284,91 @@ export class Agent {
284
284
 
285
285
 
286
286
  private async processQueue() {
287
- if (this.processing || this.inputQueue.length === 0) return;
287
+ if (this.processing) return;
288
+ if (await this.memory.queue.empty("main-session")) return ;
288
289
  if (!this.initialized) await this.init();
289
290
  this.processing = true;
290
291
 
291
- const { text, label } = this.inputQueue.shift()!;
292
+ while (true) {
293
+ const queuedItem = await this.memory.queue.pop("main-session");
294
+ if (!queuedItem) {
295
+ this.processing = false;
296
+ return;
297
+ }
298
+ const { text, label } = queuedItem;
292
299
 
293
- for (const out of this.outputAdapters) {
294
- out.onAgentStart(label);
295
- }
300
+ for (const out of this.outputAdapters) {
301
+ out.onAgentStart(label);
302
+ }
303
+
304
+ this.messages = await this.memory.compactHistory("main-session", this.model);
296
305
 
297
- this.messages = await this.memory.compactHistory("main-session", this.model);
298
-
299
- // Bootstrap: if bootstrapPrompt is set, run it instead of normal chat
300
- // until the required files (SOUL.md, IDENTITY.md, USER.md) are created
301
- const isFirstMessage = this.messages.length === 0;
302
- let input: string;
303
- if (this.bootstrapPrompt && isFirstMessage) {
304
- input = `[BOOTSTRAP MODE] The workspace is not yet set up.\n\n` +
305
- `${this.bootstrapPrompt}\n\n` +
306
- `Use fs.writeFile to create each file in the workspace when the user provides the information.\n\n` +
307
- `--- USER MESSAGE ---\n${text}`;
308
- } else if (this.bootstrapPrompt) {
309
- // Still in bootstrap mode (subsequent messages) — check if bootstrap is complete
310
- const workspaceDir = path.join(process.cwd(), 'workspace');
311
- const requiredFiles = ['SOUL.md', 'IDENTITY.md', 'USER.md'];
312
- let allExist = true;
313
- for (const file of requiredFiles) {
314
- try {
315
- await access(path.join(workspaceDir, file));
316
- } catch {
317
- allExist = false;
318
- break;
306
+ // Bootstrap: if bootstrapPrompt is set, run it instead of normal chat
307
+ // until the required files (SOUL.md, IDENTITY.md, USER.md) are created
308
+ const isFirstMessage = this.messages.length === 0;
309
+ let input: string;
310
+ if (this.bootstrapPrompt && isFirstMessage) {
311
+ input = `[BOOTSTRAP MODE] The workspace is not yet set up.\n\n` +
312
+ `${this.bootstrapPrompt}\n\n` +
313
+ `Use fs.writeFile to create each file in the workspace when the user provides the information.\n\n` +
314
+ `--- USER MESSAGE ---\n${text}`;
315
+ } else if (this.bootstrapPrompt) {
316
+ // Still in bootstrap mode (subsequent messages) — check if bootstrap is complete
317
+ const workspaceDir = path.join(process.cwd(), 'workspace');
318
+ const requiredFiles = ['SOUL.md', 'IDENTITY.md', 'USER.md'];
319
+ let allExist = true;
320
+ for (const file of requiredFiles) {
321
+ try {
322
+ await access(path.join(workspaceDir, file));
323
+ } catch {
324
+ allExist = false;
325
+ break;
326
+ }
319
327
  }
328
+ if (allExist) {
329
+ this.bootstrapPrompt = null;
330
+ console.log(` ✅ Bootstrap complete! SOUL.md, IDENTITY.md, and USER.md are now present.`);
331
+ }
332
+ input = text;
333
+ } else if (isFirstMessage) {
334
+ input = `[SYSTEM BOOT] This is a fresh session. Before responding to the user, you MUST execute the "Every Session" protocol from AGENTS.md NOW using your tools:\n` +
335
+ `1. Call fs.readFile for SOUL.md\n` +
336
+ `2. Call fs.readFile for USER.md\n` +
337
+ `3. Call fs.readFile for memory:${getTodayString()}.md (create it with fs.writeFile if it doesn't exist)\n` +
338
+ `4. Call fs.readFile for MEMORY.md\n` +
339
+ `Execute ALL of these tool calls first, then respond to the user's message below.\n\n` +
340
+ `--- USER MESSAGE ---\n${text}`;
341
+ } else {
342
+ input = text;
320
343
  }
321
- if (allExist) {
322
- this.bootstrapPrompt = null;
323
- console.log(` ✅ Bootstrap complete! SOUL.md, IDENTITY.md, and USER.md are now present.`);
324
- }
325
- input = text;
326
- } else if (isFirstMessage) {
327
- input = `[SYSTEM BOOT] This is a fresh session. Before responding to the user, you MUST execute the "Every Session" protocol from AGENTS.md NOW using your tools:\n` +
328
- `1. Call fs.readFile for SOUL.md\n` +
329
- `2. Call fs.readFile for USER.md\n` +
330
- `3. Call fs.readFile for memory:${getTodayString()}.md (create it with fs.writeFile if it doesn't exist)\n` +
331
- `4. Call fs.readFile for MEMORY.md\n` +
332
- `Execute ALL of these tool calls first, then respond to the user's message below.\n\n` +
333
- `--- USER MESSAGE ---\n${text}`;
334
- } else {
335
- input = text;
336
- }
337
344
 
338
- let fullResponse = "";
339
- try {
340
- const newMessages = await runAgent(input, this.memory, this.model, this.messages, this.tools, (chunk) => {
341
- fullResponse += chunk;
342
- for (const out of this.outputAdapters) {
343
- out.onResponseChunk(chunk);
345
+ let fullResponse = "";
346
+ try {
347
+ const newMessages = await runAgent(input, this.memory, this.model, this.messages, this.tools, (chunk) => {
348
+ fullResponse += chunk;
349
+ for (const out of this.outputAdapters) {
350
+ out.onResponseChunk(chunk);
351
+ }
352
+ });
353
+
354
+ for (const msg of newMessages) {
355
+ if (typeof msg.content !== "string") {
356
+ msg.content = msg.content.filter((part) => part.type !== 'reasoning');
357
+ }
358
+ await this.memory.history.push("main-session", msg);
344
359
  }
345
- });
346
360
 
347
- for (const msg of newMessages) {
348
- if (typeof msg.content !== "string") {
349
- msg.content = msg.content.filter((part) => part.type !== 'reasoning');
361
+ for (const out of this.outputAdapters) {
362
+ out.onResponseEnd(fullResponse);
350
363
  }
351
- await this.memory.history.push("main-session", msg);
352
- }
353
-
354
- for (const out of this.outputAdapters) {
355
- out.onResponseEnd(fullResponse);
356
- }
357
364
 
358
- // Compact history if it's grown past the threshold
359
- this.messages = await this.memory.compactHistory("main-session", this.model);
360
- } catch (error: any) {
361
- for (const out of this.outputAdapters) {
362
- out.onError(error);
365
+ // Compact history if it's grown past the threshold
366
+ this.messages = await this.memory.compactHistory("main-session", this.model);
367
+ } catch (error: any) {
368
+ for (const out of this.outputAdapters) {
369
+ out.onError(error);
370
+ }
363
371
  }
364
372
  }
365
-
366
- this.processing = false;
367
- this.processQueue();
368
373
  }
369
374
  }
@@ -3,17 +3,17 @@ description: "Agent should use an installed skill instead of hallucinating the a
3
3
 
4
4
  setup:
5
5
  files:
6
+ IDENTITY.md: "I am a bot"
7
+ USER.md: "Mr X"
8
+ SOUL.md: "I will not install skills, I will check if they are available and use them but before I install them: I will ask first."
6
9
  # Simulate an already installed skill file
7
- skills/calculator/SKILL.md: "I am a calculator."
8
- permissions:
9
- calculator:
10
- allowed: true
10
+ skills/oracle/SKILL.md: "I am an oracle. If somebody asks a question, I will answer with 42. Make sure that they tell that the oracle told them."
11
11
 
12
- input: "Ask the 'calculator' skill what 1337 times 42 is."
12
+ input: "Ask the 'oracle' skill, what is the answer to my question."
13
13
 
14
14
  validate:
15
15
  response:
16
- contains_any: ["calculator", "skill", "56154", "1337", "42"]
16
+ contains_any: ["oracle", "42"]
17
17
  llm_eval: |
18
- Does the agent response reference the calculator skill?
18
+ Does the agent response reference the oracle skill?
19
19
  The response should indicate the agent attempted to delegate to the skill rather than answering entirely on its own.
package/src/memory.ts CHANGED
@@ -2,7 +2,7 @@ import { createStorage, type Storage } from "unstorage";
2
2
  import fsDriver from "unstorage/drivers/fs";
3
3
  import { generateText, type LanguageModel, type ModelMessage } from "ai";
4
4
  import path from "path";
5
- import { LibSqlKeyValueStorage, LibSqlListStorage } from "./storage.js";
5
+ import { LibSqlKeyValueStorage, LibSqlListStorage, LibSqlFiFoStorage } from "./storage.js";
6
6
  import { logger } from './logger.js';
7
7
  import memoryDriver from 'unstorage/drivers/memory';
8
8
 
@@ -21,17 +21,22 @@ export class AgentMemory {
21
21
  // 3. Workspace (Unstorage - ./workspace)
22
22
  public workspace: Storage;
23
23
 
24
- private constructor(secrets: LibSqlKeyValueStorage, history: LibSqlListStorage<ModelMessage>, workspace: Storage) {
24
+ // 4. Fifo Queue (libSQL - file:queue.db)
25
+ public queue: LibSqlFiFoStorage<{ text: string, label: string}>;
26
+
27
+ private constructor(secrets: LibSqlKeyValueStorage, history: LibSqlListStorage<ModelMessage>, workspace: Storage, queue: LibSqlFiFoStorage<{ text: string, label: string}>) {
25
28
  this.secrets = secrets;
26
29
  this.history = history;
27
- this.workspace = workspace
30
+ this.workspace = workspace;
31
+ this.queue = queue;
28
32
  }
29
33
 
30
34
  static async createInMemory() {
31
35
  return new AgentMemory(
32
36
  await LibSqlKeyValueStorage.create(':memory:'),
33
37
  await LibSqlListStorage.create<ModelMessage>(':memory:'),
34
- createStorage({ driver: memoryDriver() })
38
+ createStorage({ driver: memoryDriver() }),
39
+ await LibSqlFiFoStorage.create(':memory:')
35
40
  );
36
41
  }
37
42
 
@@ -47,7 +52,11 @@ export class AgentMemory {
47
52
  ),
48
53
  createStorage({
49
54
  driver: fsDriver({ base: path.join(process.cwd(), "workspace") })
50
- })
55
+ }),
56
+ await LibSqlFiFoStorage.create(
57
+ process.env.QUEUE_DB_URL || "file:queue.db",
58
+ process.env.QUEUE_AUTH_TOKEN
59
+ )
51
60
  );
52
61
  }
53
62
 
package/src/storage.ts CHANGED
@@ -152,9 +152,14 @@ export class LibSqlFiFoStorage<T> {
152
152
  private client: Client;
153
153
  private tableName = 'queue_items';
154
154
 
155
- constructor(url: string, authToken?: string) {
155
+ private constructor(url: string, authToken?: string) {
156
156
  this.client = authToken ? createClient({ url, authToken }) : createClient({ url });
157
- this.init();
157
+ }
158
+
159
+ static async create<T>(url: string, authToken?: string): Promise<LibSqlFiFoStorage<T>> {
160
+ const storage = new LibSqlFiFoStorage<T>(url, authToken);
161
+ await storage.init();
162
+ return storage;
158
163
  }
159
164
 
160
165
  private async init(): Promise<void> {