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 +1 -1
- package/src/agent.eval.test.ts +9 -2
- package/src/agent.ts +77 -72
- package/src/evals/skill_sandbox_execution.yaml +7 -7
- package/src/memory.ts +14 -5
- package/src/storage.ts +7 -2
package/package.json
CHANGED
package/src/agent.eval.test.ts
CHANGED
|
@@ -94,8 +94,15 @@ const runTestCaseFile = async (filename: string) => {
|
|
|
94
94
|
onError: (e) => { throw e; }
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
(
|
|
98
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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/
|
|
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 '
|
|
12
|
+
input: "Ask the 'oracle' skill, what is the answer to my question."
|
|
13
13
|
|
|
14
14
|
validate:
|
|
15
15
|
response:
|
|
16
|
-
contains_any: ["
|
|
16
|
+
contains_any: ["oracle", "42"]
|
|
17
17
|
llm_eval: |
|
|
18
|
-
Does the agent response reference the
|
|
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
|
-
|
|
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
|
-
|
|
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> {
|