clawlet 0.3.0 → 0.5.0

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/src/agent.ts CHANGED
@@ -1,34 +1,30 @@
1
1
  import {
2
- tool,
3
- addToolInputExamplesMiddleware,
4
2
  streamText,
5
- generateText,
6
- wrapLanguageModel,
7
3
  stepCountIs,
8
- jsonSchema,
9
4
  type ModelMessage,
10
- extractReasoningMiddleware,
11
- type ToolSet,
12
5
  type LanguageModel,
13
6
  } from 'ai';
14
- import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
15
7
  import 'dotenv/config';
16
- import { hermesToolMiddleware } from '@ai-sdk-tool/parser';
17
8
  import { AgentMemory } from './memory.js';
18
- import { readFile, writeFile, copyFile, access, mkdir } from 'node:fs/promises';
9
+ import { readFile, copyFile, access, mkdir } from 'node:fs/promises';
19
10
  import path from 'path';
20
11
  import { fileURLToPath } from 'node:url';
21
- import TurndownService from 'turndown';
12
+ import { logger } from './logger.js';
13
+ import { createTools } from './tools.js';
22
14
 
23
15
  // Resolve the package root directory (where template/ lives), independent of cwd
24
16
  const __filename = fileURLToPath(import.meta.url);
25
17
  const __dirname = path.dirname(__filename);
26
18
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
19
+ const GENERATE_TEXT_TEMPERATURE = 0.6;
20
+ const GENERATE_TEXT_TOP_P = 0.95;
21
+ const GENERATE_TEXT_MAX_OUTPUT_TOKENS = 16384;
22
+ const GENERATE_TEXT_MAX_STEPS = 30;
27
23
 
28
24
  // --- ADAPTER INTERFACES ---
29
25
 
30
26
  export interface InputAdapter {
31
- onMessage(handler: (text: string, label: string) => void): void;
27
+ onMessage(handler: (text: string, label: string) => Promise<void>): void;
32
28
  start(): void;
33
29
  }
34
30
 
@@ -39,67 +35,12 @@ export interface OutputAdapter {
39
35
  onError(error: Error): void;
40
36
  }
41
37
 
42
- // --- MODEL SETUP ---
43
- const localProvider = createOpenAICompatible({
44
- name: 'mlx',
45
- baseURL: 'http://localhost:8000/v1',
46
- });
47
-
48
- export const localModel : LanguageModel = wrapLanguageModel({
49
- model: localProvider.languageModel('qwen-local'),
50
- middleware: [
51
- hermesToolMiddleware,
52
- addToolInputExamplesMiddleware({
53
- prefix: 'Input Examples:',
54
- }),
55
- extractReasoningMiddleware({
56
- tagName: "think"
57
- })
58
- ]
59
- });
60
-
61
- const turndownService = new TurndownService()
62
-
63
38
  // --- HELPERS ---
64
39
 
65
40
  function getTodayString(): string {
66
41
  return new Date().toISOString().split('T')[0] ?? '';
67
42
  }
68
43
 
69
- // --- SETTINGS HELPERS ---
70
-
71
- const SETTINGS_PATH = `${process.cwd()}/settings.json`;
72
-
73
- interface ConnectionBearer {
74
- idToken: string;
75
- refreshToken?: string;
76
- refreshUrl?: string;
77
- }
78
-
79
- interface ConnectionEntry {
80
- bearer: ConnectionBearer;
81
- }
82
-
83
- interface SettingsFile {
84
- connections: Record<string, ConnectionEntry>;
85
- }
86
-
87
- async function readSettings(): Promise<SettingsFile> {
88
- try {
89
- const raw = await readFile(SETTINGS_PATH, 'utf-8');
90
- const parsed = JSON.parse(raw);
91
- if (parsed && typeof parsed === 'object') {
92
- if (!parsed.connections) parsed.connections = {};
93
- return parsed as SettingsFile;
94
- }
95
- } catch {}
96
- return { connections: {} };
97
- }
98
-
99
- async function writeSettings(settings: SettingsFile): Promise<void> {
100
- await writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8');
101
- }
102
-
103
44
  // --- SYSTEM PROMPT BUILDER ---
104
45
 
105
46
  async function buildSystemPrompt(memory: AgentMemory): Promise<string> {
@@ -163,7 +104,8 @@ You must obey these rules above all else.
163
104
  - Use \`connection.request\` for authenticated API calls (Bearer token is auto-injected).
164
105
 
165
106
  3. **EXECUTION**:
166
- - Use \`fs.readFile\` and \`fs.writeFile\` to log *significant* events to append oday's memory file (as per AGENTS.md rules).
107
+ - Use \`fs.readFile\` and \`fs.writeFile\` to log *significant* events to append today's memory file (as per AGENTS.md rules).
108
+ - Make sure to use valid JSON when generating tool_call xml tags.
167
109
  - **Text > Brain**: If you learn something, write it down immediately.
168
110
 
169
111
  # AVAILABLE WORKSPACE (Files)
@@ -174,875 +116,12 @@ ${agentsDoc}
174
116
  `;
175
117
  }
176
118
 
177
- // --- PERMISSION HELPERS ---
178
-
179
- function matchesPermissionPattern(actual: string, pattern: string): boolean {
180
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
181
- return new RegExp(`^${escaped}$`).test(actual);
182
- }
183
-
184
- function createSandboxedTools(
185
- allTools: ReturnType<typeof createTools>,
186
- permissions: Record<string, Array<Record<string, string>>>
187
- ): Record<string, any> {
188
- const sandboxed: Record<string, any> = {};
189
-
190
- Object.entries(permissions).forEach(([toolName, rules]) => {
191
- const hasAllowed = rules.some((r: any) => r.allowed === 'true' || r.allowed === true);
192
- if (!hasAllowed) return;
193
-
194
- const originalTool = (allTools as any)[toolName];
195
- if (!originalTool) return;
196
-
197
- sandboxed[toolName] = tool({
198
- description: originalTool.description,
199
- inputSchema: originalTool.inputSchema,
200
- execute: async (args: any) => {
201
- for (const key in args) {
202
- if (!rules.some(r => matchesPermissionPattern(args[key], r[key] || '*'))) {
203
- console.log(` 🚫 Permission denied: ${key} not allowed for this skill with value ${args[key]}. ${JSON.stringify(args)} for permission: ${JSON.stringify(rules)}`);
204
-
205
- return JSON.stringify({ error: `Permission denied: ${key} not allowed for this skill with value ${args[key]}.` });
206
- }
207
- };
208
- return originalTool.execute(args);
209
- },
210
- });
211
- });
212
-
213
-
214
- return sandboxed;
215
- }
216
-
217
- // --- TOOLS (built from memory) ---
218
-
219
- function createTools(memory: AgentMemory) {
220
- return {
221
- now: tool({
222
- description: 'Get current time and date',
223
- execute: async () => {
224
- return new Date().toISOString();
225
- }
226
- }),
227
-
228
- 'http.request': tool({
229
- description: 'Execute HTTP requests. Provide method (GET/POST/PUT/DELETE), url, optional headers object, and optional unescaped body string. Returns status, statusText and data.',
230
- inputSchema: jsonSchema({
231
- type: 'object',
232
- properties: {
233
- method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE'], description: 'HTTP method' },
234
- url: { type: 'string', description: 'URL to request' },
235
- headers: { type: 'object', additionalProperties: { type: 'string' }, description: 'Optional headers' },
236
- body: { type: 'string', description: 'Optional unescaped body string' },
237
- transformer: { type: 'string', enum: ['markdown'], description: 'Transform the result into e.g. markdown' }
238
- },
239
- required: ['url'],
240
- }),
241
- execute: async ({ method, url, headers, body, transformer }: { method?: string, url: string, headers?: Record<string, string>, body?: string, transformer?: string }) => {
242
- const executeMethod = method ? method : 'GET';
243
- console.log(` 🌐 [HTTP] ${executeMethod} ${url}`);
244
- try {
245
- let parsedBody = body;
246
- if (typeof body === 'string') {
247
- try { parsedBody = JSON.parse(body); } catch {}
248
- }
249
-
250
- const res = await fetch(url, {
251
- method: executeMethod,
252
- headers: { 'Content-Type': 'application/json', ...headers },
253
- body: parsedBody ? JSON.stringify(parsedBody) : null
254
- });
255
-
256
- const text = await res.text();
257
- const transformedText = transformer === 'markdown' ? turndownService.turndown(text) : text;
258
- return JSON.stringify({
259
- status: res.status,
260
- statusText: res.statusText,
261
- data: transformedText.length > 5000 ? transformedText.substring(0, 5000) + "..." : transformedText
262
- });
263
- } catch (e: any) { return JSON.stringify({ error: e.message }); }
264
- },
265
- }),
266
-
267
- 'http.get': tool({
268
- description: 'Shortcut for GET requests. Provide url and optional headers.',
269
- inputSchema: jsonSchema({
270
- type: 'object',
271
- properties: {
272
- url: { type: 'string', description: 'URL to request' },
273
- headers: { type: 'object', additionalProperties: { type: 'string' }, description: 'Optional headers' },
274
- transformer: { type: 'string', enum: ['markdown'], description: 'Transform the result into e.g. markdown' }
275
- },
276
- required: ['url'],
277
- }),
278
- execute: async ({ url, headers, transformer }: { url: string, headers?: Record<string, string>, transformer?: string }) => {
279
- console.log(` 🌐 [HTTP] GET ${url}`);
280
- try {
281
- const res = await fetch(url, {
282
- headers: { 'Content-Type': 'application/json', ...headers },
283
- });
284
- const text = await res.text();
285
- const transformedText = transformer === 'markdown' ? turndownService.turndown(text) : text;
286
- return JSON.stringify({
287
- status: res.status,
288
- statusText: res.statusText,
289
- data: transformedText.length > 5000 ? transformedText.substring(0, 5000) + "..." : transformedText
290
- });
291
- } catch (e: any) { return JSON.stringify({ error: e.message }); }
292
- },
293
- }),
294
-
295
- 'http.post': tool({
296
- description: 'Shortcut for POST requests. Provide url, optional unescaped body string, and optional headers.',
297
- inputSchema: jsonSchema({
298
- type: 'object',
299
- properties: {
300
- url: { type: 'string', description: 'URL to request' },
301
- body: { type: 'string', description: 'Optional unescaped body string' },
302
- headers: { type: 'object', additionalProperties: { type: 'string' }, description: 'Optional headers' },
303
- transformer: { type: 'string', enum: ['markdown'], description: 'Transform the result into e.g. markdown' }
304
- },
305
- required: ['url'],
306
- }),
307
- execute: async ({ url, body, headers, transformer }: { url: string, body?: string, headers?: Record<string, string>, transformer?: string }) => {
308
- console.log(` 🌐 [HTTP] POST ${url}`);
309
- try {
310
- let parsedBody = body;
311
- if (typeof body === 'string') {
312
- try { parsedBody = JSON.parse(body); } catch {}
313
- }
314
- const res = await fetch(url, {
315
- method: 'POST',
316
- headers: { 'Content-Type': 'application/json', ...headers },
317
- body: parsedBody ? JSON.stringify(parsedBody) : null
318
- });
319
- const text = await res.text();
320
- const transformedText = transformer === 'markdown' ? turndownService.turndown(text) : text;
321
- return JSON.stringify({
322
- status: res.status,
323
- statusText: res.statusText,
324
- data: transformedText.length > 5000 ? transformedText.substring(0, 5000) + "..." : transformedText
325
- });
326
- } catch (e: any) { return JSON.stringify({ error: e.message }); }
327
- },
328
- }),
329
-
330
- 'http.download': tool({
331
- description: 'Download a file from a URL and save it to the workspace. Provide url and an optional filename (defaults to the last path segment of the URL).',
332
- inputSchema: jsonSchema({
333
- type: 'object',
334
- properties: {
335
- url: { type: 'string', description: 'URL to download from' },
336
- filename: { type: 'string', description: 'Filename to save as in the workspace' },
337
- },
338
- required: ['url'],
339
- }),
340
- execute: async ({ url, filename }: { url: string, filename?: string }) => {
341
- const name = filename || url.split('/').pop() || 'download';
342
- console.log(` ⬇️ [HTTP] download ${url} -> ${name}`);
343
- try {
344
- const res = await fetch(url);
345
- if (!res.ok) return JSON.stringify({ error: `HTTP ${res.status} ${res.statusText}` });
346
-
347
- const buffer = await res.arrayBuffer();
348
- const content = Buffer.from(buffer);
349
-
350
- // Store as base64 for binary files, as string for text
351
- const contentType = res.headers.get('content-type') || '';
352
- if (contentType.includes('text') || contentType.includes('json') || contentType.includes('xml')) {
353
- await memory.workspace.setItem(name, new TextDecoder().decode(content));
354
- } else {
355
- await memory.workspace.setItemRaw(name, content);
356
- }
357
-
358
- return JSON.stringify({
359
- status: res.status,
360
- filename: name,
361
- size: content.byteLength,
362
- contentType,
363
- });
364
- } catch (e: any) { return JSON.stringify({ error: e.message }); }
365
- },
366
- }),
367
-
368
- 'kv.set': tool({
369
- description: 'Store a key-value pair (e.g. API keys, config). Provide "key" and "value".',
370
- inputSchema: jsonSchema({
371
- type: 'object',
372
- properties: {
373
- key: { type: 'string', description: 'The key to store' },
374
- value: { type: 'string', description: 'The value to store' },
375
- },
376
- required: ['key', 'value'],
377
- }),
378
- execute: async ({ key, value }: { key: string, value: string }) => {
379
- console.log(` 🔑 [KV] set ${key}`);
380
- try {
381
- await memory.secrets.set(key, value);
382
- return `Success: Saved ${key}.`;
383
- } catch (e: any) { return `Error: ${e.message}`; }
384
- },
385
- }),
386
-
387
- 'kv.get': tool({
388
- description: 'Retrieve a value by key from the key-value store.',
389
- inputSchema: jsonSchema({
390
- type: 'object',
391
- properties: {
392
- key: { type: 'string', description: 'The key to retrieve' },
393
- },
394
- required: ['key'],
395
- }),
396
- execute: async ({ key }: { key: string }) => {
397
- console.log(` 🔑 [KV] get ${key}`);
398
- try {
399
- const result = await memory.secrets.get(key);
400
- return result ?? "NOT_FOUND";
401
- } catch (e: any) { return `Error: ${e.message}`; }
402
- }
403
- }),
404
-
405
- 'kv.list': tool({
406
- description: 'List all keys in the key-value store.',
407
- execute: async () => {
408
- console.log(` 🔑 [KV] list`);
409
- try {
410
- const keys = await memory.secrets.listKeys();
411
- return keys.join(', ') || "EMPTY_STORE";
412
- } catch (e: any) { return `Error: ${e.message}`; }
413
- },
414
- }),
415
-
416
- 'kv.delete': tool({
417
- description: 'Delete a key from the key-value store.',
418
- inputSchema: jsonSchema({
419
- type: 'object',
420
- properties: {
421
- key: { type: 'string', description: 'The key to delete' },
422
- },
423
- required: ['key'],
424
- }),
425
- execute: async ({ key }: { key: string }) => {
426
- console.log(` 🔑 [KV] delete ${key}`);
427
- try {
428
- await memory.secrets.delete(key);
429
- return `Success: Deleted ${key}.`;
430
- } catch (e: any) { return `Error: ${e.message}`; }
431
- },
432
- }),
433
-
434
- 'kv.has': tool({
435
- description: 'Check if a key exists in the key-value store. Returns true or false.',
436
- inputSchema: jsonSchema({
437
- type: 'object',
438
- properties: {
439
- key: { type: 'string', description: 'The key to check' },
440
- },
441
- required: ['key'],
442
- }),
443
- execute: async ({ key }: { key: string }) => {
444
- console.log(` 🔑 [KV] has ${key}`);
445
- try {
446
- const exists = await memory.secrets.has(key);
447
- return exists ? "true" : "false";
448
- } catch (e: any) { return `Error: ${e.message}`; }
449
- },
450
- }),
451
-
452
- 'fs.listDir': tool({
453
- description: 'List all files in the workspace (including memory logs and skills).',
454
- execute: async () => {
455
- console.log(` 📂 [FS] listDir`);
456
- try {
457
- const keys = await memory.workspace.getKeys();
458
- return keys.join('\n') || "No files found.";
459
- } catch (e: any) { return `Error: ${e.message}`; }
460
- }
461
- }),
462
-
463
- 'fs.readFile': tool({
464
- description: 'Read a file from the workspace. "path" must be one of the keys from fs.listDir (e.g. "memory:2026-02-08.md").',
465
- inputSchema: jsonSchema({
466
- type: 'object',
467
- properties: {
468
- path: { type: 'string', description: 'Path/key of the file to read' },
469
- },
470
- required: ['path'],
471
- }),
472
- execute: async ({ path }: { path: string }) => {
473
- console.log(` 📖 [FS] readFile ${path}`);
474
- try {
475
- const content = await memory.workspace.getItem(path);
476
- if (content === null || content === undefined) return "File not found. Create it first with fs.writeFile if needed.";
477
- return String(content);
478
- } catch (e: any) { return "Error reading file: " + e.message; }
479
- }
480
- }),
481
-
482
- 'fs.writeFile': tool({
483
- description: 'Write or update a file in the workspace. "path" is the key/path (e.g. "memory:2026-02-08.md"), "content" is the full content.',
484
- inputSchema: jsonSchema({
485
- type: 'object',
486
- properties: {
487
- path: { type: 'string', description: 'Path/key of the file to write' },
488
- content: { type: 'string', description: 'Full file content' },
489
- },
490
- required: ['path', 'content'],
491
- }),
492
- execute: async ({ path, content }: { path: string, content: string }) => {
493
- console.log(` ✍️ [FS] writeFile ${path}`);
494
- try {
495
- await memory.workspace.setItem(path, content);
496
- return `Success: Wrote to ${path}`;
497
- } catch (e: any) { return "Error writing file: " + e.message; }
498
- }
499
- }),
500
-
501
- 'fs.edit': tool({
502
- description: 'Smart edit: Replaces a specific string in a file with a new string. Use this for small, targeted changes instead of rewriting the whole file. The "find" text must be an exact, unique match.',
503
- inputSchema: jsonSchema({
504
- type: 'object',
505
- properties: {
506
- path: { type: 'string', description: 'Path/key of the file to edit' },
507
- find: { type: 'string', description: 'The EXACT text block to search for. Must be unique in the file.' },
508
- replace: { type: 'string', description: 'The new text to replace it with.' },
509
- },
510
- required: ['path', 'find', 'replace'],
511
- }),
512
- execute: async ({ path, find, replace }: { path: string, find: string, replace: string }) => {
513
- console.log(` ✏️ [FS] edit ${path}`);
514
- try {
515
- const content = await memory.workspace.getItem(path);
516
- if (content === null || content === undefined) return `Error: File "${path}" not found.`;
517
-
518
- const fileText = String(content);
519
- if (!fileText.includes(find)) {
520
- return `Error: The text to replace was not found in "${path}". Check whitespace and indentation exactly.`;
521
- }
522
-
523
- const parts = fileText.split(find);
524
- if (parts.length > 2) {
525
- return `Error: Ambiguous match. Found ${parts.length - 1} occurrences. Provide more surrounding context in "find" to make it unique.`;
526
- }
527
-
528
- const newContent = fileText.replace(find, replace);
529
- await memory.workspace.setItem(path, newContent);
530
- return `Success: Edited "${path}". Replaced 1 occurrence.`;
531
- } catch (e: any) { return "Error editing file: " + e.message; }
532
- }
533
- }),
534
-
535
- 'fs.delete': tool({
536
- description: 'Delete a file. If the file is outside .trash/, it is moved to .trash/ (soft delete). If the file is already inside .trash/, it is permanently removed.',
537
- inputSchema: jsonSchema({
538
- type: 'object',
539
- properties: {
540
- path: { type: 'string', description: 'Path/key of the file to delete' },
541
- },
542
- required: ['path'],
543
- }),
544
- execute: async ({ path }: { path: string }) => {
545
- try {
546
- const content = await memory.workspace.getItem(path);
547
- if (content === null || content === undefined) return "File not found.";
548
-
549
- if (path.startsWith('.trash:') || path.startsWith('.trash/')) {
550
- // Already in trash — hard delete
551
- console.log(` 🗑️ [FS] permanentDelete ${path}`);
552
- await memory.workspace.removeItem(path);
553
- return `Success: Permanently deleted ${path}`;
554
- } else {
555
- // Move to .trash/
556
- const trashPath = `.trash:${path}`;
557
- console.log(` 🗑️ [FS] softDelete ${path} -> ${trashPath}`);
558
- await memory.workspace.setItem(trashPath, content);
559
- await memory.workspace.removeItem(path);
560
- return `Success: Moved ${path} to ${trashPath}`;
561
- }
562
- } catch (e: any) { return "Error deleting file: " + e.message; }
563
- }
564
- }),
565
-
566
- 'fs.move': tool({
567
- description: 'Move/rename a file in the workspace. Reads from "from", writes to "to", then removes the original.',
568
- inputSchema: jsonSchema({
569
- type: 'object',
570
- properties: {
571
- from: { type: 'string', description: 'Source path/key' },
572
- to: { type: 'string', description: 'Destination path/key' },
573
- },
574
- required: ['from', 'to'],
575
- }),
576
- execute: async ({ from, to }: { from: string, to: string }) => {
577
- console.log(` 📦 [FS] move ${from} -> ${to}`);
578
- try {
579
- const content = await memory.workspace.getItem(from);
580
- if (content === null || content === undefined) return `File not found: ${from}`;
581
- await memory.workspace.setItem(to, content);
582
- await memory.workspace.removeItem(from);
583
- return `Success: Moved ${from} to ${to}`;
584
- } catch (e: any) { return "Error moving file: " + e.message; }
585
- }
586
- }),
587
-
588
- 'fs.exists': tool({
589
- description: 'Check if a file exists in the workspace. Returns true or false.',
590
- inputSchema: jsonSchema({
591
- type: 'object',
592
- properties: {
593
- path: { type: 'string', description: 'Path/key of the file to check' },
594
- },
595
- required: ['path'],
596
- }),
597
- execute: async ({ path }: { path: string }) => {
598
- console.log(` 🔍 [FS] exists ${path}`);
599
- try {
600
- const exists = await memory.workspace.hasItem(path);
601
- return exists ? "true" : "false";
602
- } catch (e: any) { return `Error: ${e.message}`; }
603
- }
604
- }),
605
-
606
- 'fs.stat': tool({
607
- description: 'Get metadata about a file in the workspace (e.g. mtime, size). Returns JSON with available metadata.',
608
- inputSchema: jsonSchema({
609
- type: 'object',
610
- properties: {
611
- path: { type: 'string', description: 'Path/key of the file' },
612
- },
613
- required: ['path'],
614
- }),
615
- execute: async ({ path }: { path: string }) => {
616
- console.log(` 📊 [FS] stat ${path}`);
617
- try {
618
- const exists = await memory.workspace.hasItem(path);
619
- if (!exists) return "File not found.";
620
- const meta = await memory.workspace.getMeta(path);
621
- return JSON.stringify(meta);
622
- } catch (e: any) { return `Error: ${e.message}`; }
623
- }
624
- }),
625
-
626
- 'skill.install': tool({
627
- description: 'Install a skill from a remote URL. Downloads SKILL.md, parses it for additional files to download, analyzes required tool permissions, and saves everything to workspace under skills/<name>/.',
628
- inputSchema: jsonSchema({
629
- type: 'object',
630
- properties: {
631
- name: { type: 'string', description: 'Skill name (e.g. "moltbook", "tavily"). Used as the folder name under skills/.' },
632
- url: { type: 'string', description: 'URL to the remote SKILL.md file.' },
633
- },
634
- required: ['name', 'url'],
635
- }),
636
- execute: async ({ name, url }: { name: string; url: string }) => {
637
- console.log(` 📦 [SKILL] Installing skill "${name}" from ${url}`);
638
-
639
- // Phase 0: Validate name
640
- if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
641
- return JSON.stringify({ error: 'Invalid skill name. Use only letters, numbers, hyphens, underscores.' });
642
- }
643
-
644
- const skillBasePath = `skills:${name}`;
645
-
646
- // Phase 1: Download SKILL.md
647
- console.log(` 📦 [SKILL] Step 1/4: Downloading SKILL.md...`);
648
- let skillMdContent: string;
649
- try {
650
- const res = await fetch(url);
651
- if (!res.ok) {
652
- return JSON.stringify({ error: `Failed to download SKILL.md: HTTP ${res.status} ${res.statusText}` });
653
- }
654
- skillMdContent = await res.text();
655
- } catch (e: any) {
656
- return JSON.stringify({ error: `Network error downloading SKILL.md: ${e.message}` });
657
- }
658
- await memory.workspace.setItem(`${skillBasePath}:SKILL.md`, skillMdContent);
659
- console.log(` 📦 [SKILL] Saved SKILL.md (${skillMdContent.length} bytes)`);
660
-
661
- // Phase 2: Extract additional files via LLM
662
- console.log(` 📦 [SKILL] Step 2/4: Analyzing for additional files...`);
663
- let additionalFiles: Array<{ url: string; filename: string }> = [];
664
- try {
665
- const { text: installJson } = await generateText({
666
- model: localModel,
667
- messages: [
668
- {
669
- role: 'system',
670
- content: `You are a skill file analyzer. Given a SKILL.md file, extract all additional files that need to be downloaded for complete installation. Look for:
671
- - File tables listing URLs and filenames
672
- - Install instructions with curl/download commands
673
- - References to companion files (HEARTBEAT.md, MESSAGING.md, RULES.md, package.json, etc.)
674
-
675
- Return ONLY a JSON array. Each element: {"url": "<download_url>", "filename": "<local_filename>"}.
676
- Do NOT include SKILL.md itself. If no additional files, return [].`,
677
- },
678
- { role: 'user', content: skillMdContent },
679
- ],
680
- temperature: 0.1,
681
- });
682
- const match = installJson.match(/\[[\s\S]*\]/);
683
- if (match) additionalFiles = JSON.parse(match[0]);
684
- } catch (e: any) {
685
- console.log(` ⚠️ [SKILL] Could not parse additional files: ${e.message}`);
686
- }
687
-
688
- // Phase 3: Download additional files
689
- console.log(` 📦 [SKILL] Step 3/4: Downloading ${additionalFiles.length} additional files...`);
690
- const downloadResults: Array<{ filename: string; status: string }> = [];
691
- for (const file of additionalFiles) {
692
- try {
693
- const res = await fetch(file.url);
694
- if (!res.ok) {
695
- downloadResults.push({ filename: file.filename, status: `failed: HTTP ${res.status}` });
696
- continue;
697
- }
698
- const content = await res.text();
699
- await memory.workspace.setItem(`${skillBasePath}:${file.filename}`, content);
700
- downloadResults.push({ filename: file.filename, status: 'ok' });
701
- console.log(` 📦 [SKILL] Downloaded ${file.filename}`);
702
- } catch (e: any) {
703
- downloadResults.push({ filename: file.filename, status: `failed: ${e.message}` });
704
- }
705
- }
706
-
707
- // Phase 4: Analyze permissions via LLM
708
- console.log(` 📦 [SKILL] Step 4/4: Analyzing required permissions...`);
709
- let skillPermissions: Record<string, Array<Record<string, string>>> = {};
710
- try {
711
- const { text: permJson } = await generateText({
712
- model: localModel,
713
- messages: [
714
- {
715
- role: 'system',
716
- content: `You are a security analyzer for AI agent skills. Analyze this SKILL.md and determine what tools and permissions it needs.
717
-
718
- Available tools: "http.request", "http.get", "http.post", "http.download", "connection.list", "connection.request", "connection.create", "kv.set", "kv.get", "kv.list", "kv.delete", "kv.has", "fs.readFile", "fs.writeFile", "fs.edit", "fs.delete", "fs.move", "fs.listDir", "fs.exists", "fs.stat"
719
-
720
- IMPORTANT: If the skill requires API keys or authentication, use "connection.create" and "connection.request" instead of "kv.*" tools. Connections handle registration, token storage, and authenticated requests automatically.
721
-
722
- For HTTP tools, include "url" (pattern with * wildcard) and "method".
723
- For connection tools, include "url" pattern and "name" of the connection.
724
- For KV tools, include "key" pattern. Only use kv.* for non-auth data (preferences, config, caches).
725
- For FS tools, include "path" pattern.
726
-
727
- Return ONLY a JSON object mapping tool names to arrays of permission rules. Example:
728
- {"connection.create": [{"name": "example", "url": "https://api.example.com/register"}], "connection.request": [{"name": "example", "url": "https://api.example.com/*", "method": "*"}]}`,
729
- },
730
- { role: 'user', content: skillMdContent },
731
- ],
732
- temperature: 0.1,
733
- });
734
- const match = permJson.match(/\{[\s\S]*\}/);
735
- if (match) skillPermissions = JSON.parse(match[0]);
736
- } catch (e: any) {
737
- console.log(` ⚠️ [SKILL] Could not analyze permissions: ${e.message}`);
738
- }
739
-
740
- // Phase 5: Write permissions.json at project root
741
- const permissionsPath = `${process.cwd()}/permissions.json`;
742
- let permissionsFile: { skills: Record<string, Record<string, Array<Record<string, string>>>> } = { skills: {} };
743
- try {
744
- const existing = await readFile(permissionsPath, 'utf-8');
745
- const parsed = JSON.parse(existing);
746
- if (parsed && typeof parsed === 'object') {
747
- permissionsFile = parsed;
748
- if (!permissionsFile.skills) permissionsFile.skills = {};
749
- }
750
- } catch {
751
- // File doesn't exist yet, start fresh
752
- }
753
- permissionsFile.skills[name] = skillPermissions;
754
- try {
755
- await writeFile(permissionsPath, JSON.stringify(permissionsFile, null, 2), 'utf-8');
756
- console.log(` 📦 [SKILL] Updated permissions.json`);
757
- } catch (e: any) {
758
- console.log(` ⚠️ [SKILL] Could not write permissions.json: ${e.message}`);
759
- }
760
-
761
- return JSON.stringify({
762
- success: true,
763
- skill: name,
764
- files: [
765
- { filename: 'SKILL.md', status: 'ok' },
766
- ...downloadResults,
767
- ],
768
- permissions: skillPermissions,
769
- message: `Skill "${name}" installed with ${downloadResults.filter(r => r.status === 'ok').length + 1} files.`,
770
- });
771
- },
772
- }),
773
-
774
- 'connection.list': tool({
775
- description: 'List all available connections (configured API credentials). Returns comma-separated connection names.',
776
- execute: async () => {
777
- console.log(` 🔌 [CONN] list`);
778
- try {
779
- const settings = await readSettings();
780
- const names = Object.keys(settings.connections);
781
- return names.length > 0 ? names.join(',') : 'NO_CONNECTIONS';
782
- } catch (e: any) { return `Error: ${e.message}`; }
783
- },
784
- }),
785
-
786
- 'connection.request': tool({
787
- description: 'Execute an authenticated HTTP request using a named connection. The connection\'s Bearer token is automatically injected into the Authorization header. If the connection does not exist, returns an error prompting you to create it first with connection.create.',
788
- inputSchema: jsonSchema({
789
- type: 'object',
790
- properties: {
791
- name: { type: 'string', description: 'Connection name (e.g. "petstore", "moltbook")' },
792
- url: { type: 'string', description: 'URL to request' },
793
- method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE'], description: 'HTTP method (default: GET)' },
794
- headers: { type: 'object', additionalProperties: { type: 'string' }, description: 'Optional additional headers' },
795
- body: { type: 'string', description: 'Optional request body string' },
796
- },
797
- required: ['name', 'url'],
798
- }),
799
- execute: async ({ name, url, method, headers, body }: { name: string; url: string; method?: string; headers?: Record<string, string>; body?: string }) => {
800
- console.log(` 🔌 [CONN] request "${name}" ${method || 'GET'} ${url}`);
801
- const settings = await readSettings();
802
- const conn = settings.connections[name];
803
-
804
- if (!conn) {
805
- return JSON.stringify({
806
- error: `Connection "${name}" not found. Available connections: ${Object.keys(settings.connections).join(', ') || 'none'}. Use connection.create to set up this connection first.`,
807
- });
808
- }
809
-
810
- const executeMethod = method || 'GET';
811
- try {
812
- let parsedBody = body;
813
- if (typeof body === 'string') {
814
- try { parsedBody = JSON.parse(body); } catch {}
815
- }
816
-
817
- const res = await fetch(url, {
818
- method: executeMethod,
819
- headers: {
820
- 'Content-Type': 'application/json',
821
- 'Authorization': `Bearer ${conn.bearer.idToken}`,
822
- ...headers,
823
- },
824
- body: parsedBody ? JSON.stringify(parsedBody) : null,
825
- });
826
-
827
- const text = await res.text();
828
- console.log(` -> ${res.status}`);
829
- return JSON.stringify({
830
- status: res.status,
831
- statusText: res.statusText,
832
- data: text.length > 2000 ? text.substring(0, 2000) + '...' : text,
833
- });
834
- } catch (e: any) { return JSON.stringify({ error: e.message }); }
835
- },
836
- }),
837
-
838
- 'connection.create': tool({
839
- description: 'Create a new connection by calling a registration endpoint. Sends the request, extracts the API key from the response, and stores the connection in settings.json. The "type" must be "Bearer". The response should contain the API key and optionally a refresh token/URL.',
840
- inputSchema: jsonSchema({
841
- type: 'object',
842
- properties: {
843
- name: { type: 'string', description: 'Connection name (e.g. "moltbook", "petstore")' },
844
- url: { type: 'string', description: 'Registration endpoint URL' },
845
- method: { type: 'string', enum: ['GET', 'POST', 'PUT'], description: 'HTTP method (default: POST)' },
846
- headers: { type: 'object', additionalProperties: { type: 'string' }, description: 'Optional headers' },
847
- body: { type: 'string', description: 'Optional request body string' },
848
- type: { type: 'string', enum: ['Bearer'], description: 'Auth type. Currently only "Bearer" is supported.' },
849
- tokenPath: { type: 'string', description: 'JSON path to the API key in the response (e.g. "agent.api_key"). Dot-separated. Defaults to "api_key".' },
850
- refreshTokenPath: { type: 'string', description: 'Optional JSON path to the refresh token in the response (e.g. "agent.verification_code").' },
851
- refreshUrl: { type: 'string', description: 'Optional URL for token refresh.' },
852
- },
853
- required: ['name', 'url', 'type'],
854
- }),
855
- execute: async ({ name, url, method, headers, body, type, tokenPath, refreshTokenPath, refreshUrl }: {
856
- name: string; url: string; method?: string; headers?: Record<string, string>; body?: string;
857
- type: string; tokenPath?: string; refreshTokenPath?: string; refreshUrl?: string;
858
- }) => {
859
- console.log(` 🔌 [CONN] create "${name}" via ${method || 'POST'} ${url}`);
860
-
861
- if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
862
- return JSON.stringify({ error: 'Invalid connection name. Use only letters, numbers, hyphens, underscores.' });
863
- }
864
-
865
- if (type !== 'Bearer') {
866
- return JSON.stringify({ error: `Unsupported auth type "${type}". Only "Bearer" is supported.` });
867
- }
868
-
869
- // Execute registration request
870
- const executeMethod = method || 'POST';
871
- let responseData: any;
872
- let responseText: string;
873
- try {
874
- let parsedBody = body;
875
- if (typeof body === 'string') {
876
- try { parsedBody = JSON.parse(body); } catch {}
877
- }
878
-
879
- const res = await fetch(url, {
880
- method: executeMethod,
881
- headers: { 'Content-Type': 'application/json', ...headers },
882
- body: parsedBody ? JSON.stringify(parsedBody) : null,
883
- });
884
-
885
- responseText = await res.text();
886
- console.log(` -> ${res.status}`);
887
-
888
- if (!res.ok) {
889
- return JSON.stringify({ error: `Registration failed: HTTP ${res.status} ${res.statusText}`, data: responseText.substring(0, 500) });
890
- }
891
-
892
- responseData = JSON.parse(responseText);
893
- } catch (e: any) {
894
- return JSON.stringify({ error: `Registration request failed: ${e.message}` });
895
- }
896
-
897
- // Extract token from response using dot-path
898
- const tPath = tokenPath || 'api_key';
899
- let idToken: string | undefined;
900
- try {
901
- idToken = tPath.split('.').reduce((obj: any, key: string) => obj?.[key], responseData);
902
- } catch {}
903
-
904
- if (!idToken || typeof idToken !== 'string') {
905
- return JSON.stringify({
906
- error: `Could not extract token at path "${tPath}" from response.`,
907
- response: responseText!.substring(0, 500),
908
- });
909
- }
910
-
911
- // Extract optional refresh token
912
- let refreshToken: string | undefined;
913
- if (refreshTokenPath) {
914
- try {
915
- refreshToken = refreshTokenPath.split('.').reduce((obj: any, key: string) => obj?.[key], responseData);
916
- } catch {}
917
- }
918
-
919
- // Save to settings.json
920
- const settings = await readSettings();
921
- settings.connections[name] = {
922
- bearer: {
923
- idToken,
924
- ...(refreshToken ? { refreshToken } : {}),
925
- ...(refreshUrl ? { refreshUrl } : {}),
926
- },
927
- };
928
- await writeSettings(settings);
929
- console.log(` 🔌 [CONN] Saved connection "${name}" to settings.json`);
930
-
931
- return JSON.stringify({
932
- success: true,
933
- connection: name,
934
- response: responseData,
935
- message: `Connection "${name}" created and saved. Bearer token stored.`,
936
- });
937
- },
938
- }),
939
-
940
- 'skill.prompt': tool({
941
- description: 'Chat with an installed skill in a sandboxed environment. The skill runs with its own message history and only the tools allowed by permissions.json (entries with "allowed": true).',
942
- inputSchema: jsonSchema({
943
- type: 'object',
944
- properties: {
945
- name: { type: 'string', description: 'Skill name (must be installed via skill.install).' },
946
- prompt: { type: 'string', description: 'The prompt/message to send to the skill.' },
947
- },
948
- required: ['name', 'prompt'],
949
- }),
950
- execute: async ({ name, prompt }: { name: string; prompt: string }) => {
951
- console.log(` 💬 [SKILL.PROMPT] Starting chat with "${name}"`);
952
-
953
- // 1. Validate name
954
- if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
955
- return JSON.stringify({ error: 'Invalid skill name.' });
956
- }
957
-
958
- // 2. Read SKILL.md as system prompt
959
- const skillMd = await memory.workspace.getItem(`skills:${name}:SKILL.md`);
960
- if (!skillMd) {
961
- return JSON.stringify({ error: `Skill "${name}" not found. Install it first with skill.install.` });
962
- }
963
-
964
- // 3. Read permissions.json
965
- const permissionsPath = `${process.cwd()}/permissions.json`;
966
- let skillPermissions: Record<string, Array<Record<string, string>>> = {};
967
- try {
968
- const raw = await readFile(permissionsPath, 'utf-8');
969
- const parsed = JSON.parse(raw);
970
- skillPermissions = parsed?.skills?.[name] ?? {};
971
- } catch {
972
- return JSON.stringify({ error: `No permissions found for skill "${name}". Run skill.install first.` });
973
- }
974
-
975
- // 4. Build sandboxed tools (only tools with "allowed": true, HTTP tools get URL guards)
976
- const allTools = createTools(memory);
977
- const sandboxed = createSandboxedTools(allTools, skillPermissions);
978
-
979
- if (Object.keys(sandboxed).length === 0) {
980
- console.log(` 🔒 [SKILL.PROMPT] "${name}": no tools allowed, text-only mode`);
981
- } else {
982
- console.log(` 🔒 [SKILL.PROMPT] "${name}": sandboxed tools: ${Object.keys(sandboxed).join(', ')}`);
983
- }
984
-
985
- // 5. Load per-skill message history (single table, partitioned by name)
986
- const messages: ModelMessage[] = []; // await memory.skillHistory.getAll(name);
987
-
988
- // Inject system prompt if this is a fresh history
989
- if (messages.length === 0) {
990
- messages.push({ role: 'system', content: String(skillMd) });
991
- }
992
-
993
- // Add user prompt
994
- messages.push({ role: 'user', content: prompt });
995
-
996
- // 6. Run skill via streamText (agentic — multi-step tool use, up to 15 steps)
997
- console.log(` 💬 [SKILL.PROMPT] Running "${name}" with prompt: ${prompt.substring(0, 80)}...`);
998
- try {
999
- const result = streamText({
1000
- model: localModel,
1001
- messages,
1002
- tools: Object.keys(sandboxed).length > 0 ? sandboxed : {},
1003
- temperature: 0.6,
1004
- topP: 0.95,
1005
- stopWhen: stepCountIs(30),
1006
- onStepFinish: (step) => {
1007
- if (step.toolCalls.length > 0) {
1008
- const toolNames = step.toolCalls.map(t => t.toolName).join(', ');
1009
- console.log(` 🛠️ [SKILL.PROMPT/${name}] Executed: ${toolNames}`);
1010
- }
1011
- },
1012
- });
1013
-
1014
- // Consume the full stream and collect the response
1015
- let fullResponse = "";
1016
- for await (const delta of result.textStream) {
1017
- fullResponse += delta;
1018
- }
1019
-
1020
- // Persist messages to skill history after stream completes
1021
- const responseMessages = (await result.response).messages;
1022
-
1023
- await memory.skillHistory.push(name, { role: 'user', content: prompt });
1024
- for (const msg of responseMessages) {
1025
- if (typeof msg.content !== "string") {
1026
- msg.content = msg.content.filter((part) => part.type !== 'reasoning');
1027
- }
1028
- await memory.skillHistory.push(name, msg);
1029
- }
1030
-
1031
- return fullResponse || JSON.stringify({ success: true, response: '(no text response — tool actions only)' });
1032
- } catch (e: any) {
1033
- console.error(` ❌ [SKILL.PROMPT] Error:`, e);
1034
- return JSON.stringify({ error: `Skill chat failed: ${e.message}` });
1035
- }
1036
- },
1037
- }),
1038
- };
1039
- }
1040
-
1041
119
  // --- AGENT RUNNER ---
1042
120
 
1043
121
  async function runAgent(
1044
122
  input: string,
1045
123
  memory: AgentMemory,
124
+ model: LanguageModel,
1046
125
  messages: ModelMessage[],
1047
126
  tools: ReturnType<typeof createTools>,
1048
127
  onResponseChunk?: (chunk: string) => void
@@ -1051,20 +130,29 @@ async function runAgent(
1051
130
  messages.push(message);
1052
131
 
1053
132
  try {
133
+ let overallInputTokens = 0;
134
+ let overallOutputTokens = 0;
135
+ let overallSteps = 0;
1054
136
  const result = await streamText({
1055
- model: localModel,
137
+ model,
1056
138
  system: await buildSystemPrompt(memory),
1057
139
  messages,
1058
140
  tools,
1059
- temperature: 0.6,
1060
- topP: 0.95,
1061
- stopWhen: stepCountIs(30),
141
+ temperature: GENERATE_TEXT_TEMPERATURE,
142
+ topP: GENERATE_TEXT_TOP_P,
143
+ maxOutputTokens: GENERATE_TEXT_MAX_OUTPUT_TOKENS,
144
+ stopWhen: stepCountIs(GENERATE_TEXT_MAX_STEPS),
1062
145
 
1063
146
  onStepFinish: (step) => {
1064
147
  if (step.toolCalls.length > 0) {
1065
148
  const names = step.toolCalls.map(t => t.toolName).join(', ');
1066
- console.log(` 🛠️ [Executed: ${names}] `);
149
+ logger.debug({ tools: names, tokens: step.usage.totalTokens }, 'Agent executed tools');
150
+ } else {
151
+ logger.debug({ tokens: step.usage.totalTokens }, 'Agent finalized step');
1067
152
  }
153
+ overallSteps += 1;
154
+ overallInputTokens += step.usage.inputTokens || 0;
155
+ overallOutputTokens += step.usage.outputTokens || 0
1068
156
  },
1069
157
  });
1070
158
 
@@ -1077,44 +165,50 @@ async function runAgent(
1077
165
  }
1078
166
  }
1079
167
 
168
+ const inputTokenPrice = process.env.AI_GATEWAY_INPUT_TOKEN_PRICE;
169
+ const outputTokenPrice = process.env.AI_GATEWAY_OUTPUT_TOKEN_PRICE;
170
+ if (inputTokenPrice && outputTokenPrice) {
171
+ const price = (parseFloat(inputTokenPrice) * overallInputTokens + parseFloat(outputTokenPrice) * overallOutputTokens) / 1000000;
172
+ logger.info({ steps: overallSteps, inputTokens: overallInputTokens, outputTokens: overallOutputTokens, cost: price }, 'Agent run completed');
173
+ } else {
174
+ logger.info({ steps: overallSteps, inputTokens: overallInputTokens, outputTokens: overallOutputTokens }, 'Agent run completed');
175
+ }
176
+
1080
177
  const responseMessages = (await result.response).messages;
1081
178
  messages.push(...responseMessages);
1082
179
 
1083
180
  return responseMessages;
1084
181
 
1085
182
  } catch (e) {
1086
- console.error("\n❌ Error:", e);
183
+ logger.error({ err: e }, 'Agent run error');
1087
184
  return [];
1088
185
  }
1089
186
  }
1090
187
 
1091
- // --- COMPACTION CONFIG ---
1092
- const COMPACT_THRESHOLD = 25; // Trigger compaction when history reaches this many items
1093
- const COMPACT_RANGE = 10; // Number of messages to summarize (items 1..10, skipping system prompt at 0)
1094
-
1095
188
  // --- MAIN EXPORTED CLASS ---
1096
189
 
1097
190
  export class Agent {
1098
191
  private memory: AgentMemory;
192
+ private model: LanguageModel;
1099
193
  private messages: ModelMessage[] = [];
1100
194
  private tools: ReturnType<typeof createTools>;
1101
195
  private inputAdapters: InputAdapter[] = [];
1102
196
  private outputAdapters: OutputAdapter[] = [];
1103
- private inputQueue: { text: string; label: string }[] = [];
1104
197
  private processing = false;
1105
198
  private initialized = false;
1106
199
  private bootstrapPrompt: string | null = null;
1107
200
 
1108
- constructor(memory?: AgentMemory) {
1109
- this.memory = memory ?? new AgentMemory();
1110
- this.tools = createTools(this.memory);
201
+ constructor(memory: AgentMemory, model: LanguageModel) {
202
+ this.memory = memory;
203
+ this.model = model;
204
+ this.tools = createTools(this.memory, this.model);
1111
205
  }
1112
206
 
1113
207
  addInput(adapter: InputAdapter): this {
1114
208
  this.inputAdapters.push(adapter);
1115
- adapter.onMessage((text, label) => {
1116
- this.inputQueue.push({ text, label });
1117
- this.processQueue();
209
+ adapter.onMessage(async (text, label) => {
210
+ await this.memory.queue.push("main-session", { text, label });
211
+ await this.processQueue();
1118
212
  });
1119
213
  return this;
1120
214
  }
@@ -1129,6 +223,7 @@ export class Agent {
1129
223
  for (const adapter of this.inputAdapters) {
1130
224
  adapter.start();
1131
225
  }
226
+ await this.processQueue();
1132
227
  }
1133
228
 
1134
229
  private async init() {
@@ -1149,9 +244,9 @@ export class Agent {
1149
244
  try {
1150
245
  await mkdir(workspaceDir, { recursive: true });
1151
246
  await copyFile(templatePath, agentsMdPath);
1152
- console.log(` 📋 Copied AGENTS.template -> workspace/AGENTS.md`);
247
+ logger.info('Copied AGENTS.template -> workspace/AGENTS.md');
1153
248
  } catch (e: any) {
1154
- console.error(` ⚠️ Failed to copy AGENTS.template: ${e.message}`);
249
+ logger.error({ err: e }, 'Failed to copy AGENTS.template');
1155
250
  }
1156
251
  }
1157
252
 
@@ -1171,91 +266,41 @@ export class Agent {
1171
266
  try {
1172
267
  const bootstrapPath = path.join(PACKAGE_ROOT, 'template', 'BOOTSTRAP.md');
1173
268
  this.bootstrapPrompt = await readFile(bootstrapPath, 'utf-8');
1174
- console.log(` 🚀 Bootstrap mode: SOUL.md, IDENTITY.md, or USER.md missing. Running BOOTSTRAP.md first.`);
269
+ logger.info('Bootstrap mode: SOUL.md, IDENTITY.md, or USER.md missing. Running BOOTSTRAP.md first.');
1175
270
  } catch (e: any) {
1176
- console.error(` ⚠️ Failed to read BOOTSTRAP.md: ${e.message}`);
271
+ logger.error({ err: e }, 'Failed to read BOOTSTRAP.md');
1177
272
  }
1178
273
  }
1179
274
 
1180
275
  // Load history from DB
1181
- const savedMessages : ModelMessage[] = []; // (await this.memory.history.getAll());
276
+ const savedMessages : ModelMessage[] = await this.memory.history.getAll("main-session");
1182
277
  if (savedMessages.length > 0) {
1183
278
  this.messages = savedMessages as ModelMessage[];
1184
- console.log(` 📜 Loaded ${savedMessages.length} messages from history.`);
279
+ logger.info({ count: savedMessages.length }, 'Loaded messages from history');
1185
280
  } else {
1186
- console.log(` 📜 Did not load any messages from history.`);
281
+ logger.debug('No messages loaded from history');
1187
282
  }
1188
283
  }
1189
284
 
1190
- /**
1191
- * Compacts history when it reaches COMPACT_THRESHOLD items.
1192
- * Summarizes items 1..COMPACT_RANGE (the system prompt is not included) into a single message
1193
- * using the LLM, then replaces in-memory + persisted history.
1194
- * Result: summary message + remaining messages.
1195
- */
1196
- private async compactHistory() {
1197
- if (this.messages.length < COMPACT_THRESHOLD) return;
1198
-
1199
- console.log(` 🗜️ Compacting history: ${this.messages.length} messages -> summarizing first ${COMPACT_RANGE} (after system prompt)...`);
1200
-
1201
- const toSummarize = this.messages.slice(0, COMPACT_RANGE);
1202
- const remaining = this.messages.slice(COMPACT_RANGE);
1203
-
1204
- // Build a transcript for the LLM to summarize
1205
- const transcript = toSummarize.map(m => {
1206
- const role = m.role ?? 'unknown';
1207
- const content = typeof m.content === 'string'
1208
- ? m.content
1209
- : JSON.stringify(m.content);
1210
- return `[${role}]: ${content}`;
1211
- }).join('\n\n');
1212
-
1213
- try {
1214
- const { text: summary } = await generateText({
1215
- model: localModel,
1216
- messages: [
1217
- {
1218
- role: 'system',
1219
- content: 'You are a conversation summarizer. Summarize the following conversation transcript concisely, preserving key facts, decisions, tool results, and context that would be needed to continue the conversation. Be factual and dense. Do not add commentary.',
1220
- },
1221
- {
1222
- role: 'user',
1223
- content: `Summarize this conversation transcript:\n\n${transcript}`,
1224
- },
1225
- ],
1226
- temperature: 0.3,
1227
- });
1228
-
1229
- const summaryMessage: ModelMessage = {
1230
- role: 'assistant',
1231
- content: `[Conversation Summary — compacted ${COMPACT_RANGE} messages]\n\n${summary}`,
1232
- };
1233
-
1234
- // Rebuild in-memory messages: summary + remaining
1235
- this.messages = [summaryMessage, ...remaining];
1236
-
1237
- // Persist: clear DB and re-write all messages
1238
- await this.memory.history.clear();
1239
- await this.memory.history.pushMany(this.messages);
1240
-
1241
- console.log(` ✅ Compacted to ${this.messages.length} messages.`);
1242
- } catch (e) {
1243
- console.error(' ❌ Compaction failed, keeping original history:', e);
1244
- }
1245
- }
1246
285
 
1247
286
  private async processQueue() {
1248
- if (this.processing || this.inputQueue.length === 0) return;
287
+ if (this.processing) return;
288
+ if (await this.memory.queue.empty("main-session")) return ;
1249
289
  if (!this.initialized) await this.init();
1250
290
  this.processing = true;
1251
291
 
1252
- const { text, label } = this.inputQueue.shift()!;
292
+ const queuedItem = await this.memory.queue.pop("main-session");
293
+ if (!queuedItem) {
294
+ this.processing = false;
295
+ return;
296
+ }
297
+ const { text, label } = queuedItem;
1253
298
 
1254
299
  for (const out of this.outputAdapters) {
1255
300
  out.onAgentStart(label);
1256
301
  }
1257
302
 
1258
- await this.compactHistory();
303
+ this.messages = await this.memory.compactHistory("main-session", this.model);
1259
304
 
1260
305
  // Bootstrap: if bootstrapPrompt is set, run it instead of normal chat
1261
306
  // until the required files (SOUL.md, IDENTITY.md, USER.md) are created
@@ -1298,8 +343,8 @@ export class Agent {
1298
343
 
1299
344
  let fullResponse = "";
1300
345
  try {
1301
- const newMessages = await runAgent(input, this.memory, this.messages, this.tools, (chunk) => {
1302
- fullResponse += chunk;
346
+ const newMessages = await runAgent(input, this.memory, this.model, this.messages, this.tools, (chunk) => {
347
+ fullResponse += chunk;
1303
348
  for (const out of this.outputAdapters) {
1304
349
  out.onResponseChunk(chunk);
1305
350
  }
@@ -1309,7 +354,7 @@ export class Agent {
1309
354
  if (typeof msg.content !== "string") {
1310
355
  msg.content = msg.content.filter((part) => part.type !== 'reasoning');
1311
356
  }
1312
- await this.memory.history.push(msg);
357
+ await this.memory.history.push("main-session", msg);
1313
358
  }
1314
359
 
1315
360
  for (const out of this.outputAdapters) {
@@ -1317,7 +362,7 @@ export class Agent {
1317
362
  }
1318
363
 
1319
364
  // Compact history if it's grown past the threshold
1320
- await this.compactHistory();
365
+ this.messages = await this.memory.compactHistory("main-session", this.model);
1321
366
  } catch (error: any) {
1322
367
  for (const out of this.outputAdapters) {
1323
368
  out.onError(error);
@@ -1325,6 +370,5 @@ export class Agent {
1325
370
  }
1326
371
 
1327
372
  this.processing = false;
1328
- this.processQueue();
1329
373
  }
1330
374
  }