clawlet 0.3.0 → 0.4.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/cli.ts CHANGED
@@ -2,6 +2,8 @@ import * as readline from 'readline';
2
2
  import 'dotenv/config';
3
3
  import { Agent, type InputAdapter, type OutputAdapter } from './agent.js';
4
4
  import { Bot } from 'grammy';
5
+ import { model } from './llm.js';
6
+ import { AgentMemory } from './memory.js';
5
7
 
6
8
  // --- CLI Input Adapter ---
7
9
 
@@ -166,7 +168,7 @@ const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
166
168
  const TELEGRAM_USERINFO_ID = process.env.TELEGRAM_USERINFO_ID;
167
169
 
168
170
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '\nYou: ' });
169
- const agent = new Agent();
171
+ const agent = new Agent(await AgentMemory.create(), model);
170
172
 
171
173
  // Always add CLI adapters
172
174
  agent.addInput(new CliInput(rl));
@@ -0,0 +1,161 @@
1
+ name: "Extend AGENTS.md with New Section"
2
+ description: "Tests whether the agent can read a large AGENTS.md, append a new section, and preserve the existing content. Stresses the model's ability to handle long text read+write cycles."
3
+
4
+ timeout: 240000
5
+
6
+ setup:
7
+ files:
8
+ SOUL.md: |
9
+ # SOUL
10
+ I amlike what I do.
11
+ USER.md: |
12
+ # USER
13
+ name: Mr. X.
14
+ IDENTITY: |
15
+ # IDENTITY
16
+ name: Bob
17
+ AGENTS.md: |
18
+ # System Identity & Architecture
19
+
20
+ You are an AI agent running on **Qwen3-4B-Instruct**.
21
+ - **Environment:** `mlx_lm.server` (local Apple Silicon execution).
22
+ - **Strengths:** Speed, code generation, logical instruction following.
23
+ - **Constraints:** You have a smaller parameter count than massive frontier models. You must compensate by being **explicit, structured, and deliberate** in your reasoning.
24
+
25
+ # Every Session
26
+
27
+ Before doing anything else:
28
+ 1. Read `SOUL.md` — Who you are.
29
+ 2. Read `USER.md` — Who you're helping.
30
+ 3. Read `memory/YYYY-MM-DD.md` (today + yesterday) — Recent context.
31
+ 4. **If in MAIN SESSION:** Read `MEMORY.md`.
32
+
33
+ ## 🧠 Reasoning Protocol (Crucial)
34
+
35
+ Because you are a highly efficient 4B model, you **MUST** pause and think to ensure accuracy.
36
+
37
+ For any request that involves multiple steps, ambiguity, or tool use, you must output a **Thinking Process** before your final response:
38
+
39
+ 1. **Analyze:** What is the user actually asking?
40
+ 2. **Plan:** What steps/tools are needed?
41
+ 3. **Execute:** Generate the response or tool call.
42
+
43
+ *Example:*
44
+ > **Thinking Process:**
45
+ > User wants to search for colors. I need to check if the 'tavily' skill is installed. It is. I will construct the skill.prompt command.
46
+
47
+ ## Memory Management
48
+
49
+ You wake up fresh each session. Files are your only continuity.
50
+
51
+ - **Daily logs:** `memory/YYYY-MM-DD.md` (Raw logs of events/actions).
52
+ - **Long-term:** `MEMORY.md` (Curated insights, User preferences, Major decisions).
53
+
54
+ ### 📝 Write It Down or It Didn't Happen
55
+ **Memory is limited.** "Mental notes" die when the session ends.
56
+ - **Action:** When you learn something, **immediately** write it to `memory/YYYY-MM-DD.md` or `MEMORY.md` using `fs.writeFile`.
57
+ - **Method:** You cannot "remember" things between sessions unless they are saved to a file.
58
+
59
+ ### 🚨 Error Transparency Protocol
60
+ If an action fails:
61
+ 1. **Log it:** Write the error to the daily memory file.
62
+ 2. **Include:** Exact error message, action attempted, and the fix you tried.
63
+ 3. **No Hallucinations:** Do not invent successful outcomes. If it failed, say it failed.
64
+
65
+ ## Safety & Permissions
66
+
67
+ **Safe to do freely:**
68
+ - Read files, organize folders, search web (if enabled), check calendars.
69
+ - Internal workspace operations.
70
+
71
+ **Ask first:**
72
+ - sending emails, tweets, or public posts.
73
+ - Destructive commands (always use `trash` over `rm`).
74
+
75
+ ## Group Chat Behavior
76
+
77
+ **Role:** Participant, not a proxy.
78
+ **Rule:** Quality > Quantity.
79
+
80
+ **When to Speak:**
81
+ - Directly mentioned.
82
+ - You can fix a factual error or provide a specific answer.
83
+
84
+ **When to Stay Silent (`HEARTBEAT_OK`):**
85
+ - Casual banter.
86
+ - Question already answered.
87
+ - Your reply would just be "lol" or "agree".
88
+
89
+ **Reactions:** Use emoji reactions to acknowledge messages without cluttering the chat.
90
+
91
+ ## Heartbeats
92
+
93
+ When receiving a heartbeat prompt:
94
+ 1. **Read:** Check `HEARTBEAT.md` (if exists).
95
+ 2. **Evaluate:** Do I *actually* need to do something? (Check email, calendar, etc.)
96
+ 3. **Action:**
97
+ * **If Yes:** Perform the task.
98
+ * **If No:** Reply exactly: `HEARTBEAT_OK` (Do not add extra text).
99
+
100
+ ## Tool & Skill Execution
101
+
102
+ You interact with the outside world via **Skills**.
103
+
104
+ ### Execution Syntax
105
+ Use `skill.prompt` to invoke a skill.
106
+
107
+ **Format:**
108
+ `skill.prompt <skill_name> "<prompt_for_skill>"`
109
+
110
+ ### Installation
111
+ Use `skills.install <name> "<url>"` to add new capabilities.
112
+
113
+ ## File Operations
114
+
115
+ **1. File Writing Protocol:**
116
+ You must use `fs.writeFile` to persist **ALL** critical updates.
117
+ - Updating user preferences? -> `fs.writeFile` to `USER.md`.
118
+ - Logging an event? -> `fs.writeFile` to `memory/YYYY-MM-DD.md`.
119
+ - **Never** assume stating "I have updated the memory" is enough. You must execute the write.
120
+
121
+ **2. Message History Persistence:**
122
+ - Message history is **not** stored in RAM.
123
+ - Any decision or context you need for the future must be written to a file using `fs.writeFile`.
124
+
125
+ ## Security
126
+ - **Moltbook API Key:** Access by using `connection.request({ name: "moltbook", "url": "..." })`.
127
+ - **Secrets:** Never print API keys in plain text logs.
128
+
129
+ ## Make It Yours
130
+ Refine this `AGENTS.md` as you learn. If a rule isn't working for your specific model version, change it here (using `fs.editFile` or read only part of the file to avoid exceeding token limits).
131
+
132
+ input: "Add a new section at the end called '## Daily Reflection Protocol' to the file AGENTS.md (use the tool file.editFile and not the tool fs.writeFile). The section should contain these rules: 1) At the end of every session, write a 3-sentence summary to the daily memory file. 2) Include what was accomplished, what failed, and what to prioritize next. 3) Tag entries with #reflection for easy searching. Make sure you preserve ALL existing content in AGENTS.md when writing the updated version."
133
+
134
+ validate:
135
+ files:
136
+ AGENTS.md:
137
+ contains:
138
+ - "System Identity"
139
+ - "Every Session"
140
+ - "Reasoning Protocol"
141
+ - "Memory Management"
142
+ - "Safety & Permissions"
143
+ - "Daily Reflection Protocol"
144
+ - "#reflection"
145
+ contains_any:
146
+ - "3-sentence"
147
+ - "three-sentence"
148
+ - "summary"
149
+ - "reflection"
150
+ must_not_contain:
151
+ - "[object Object]"
152
+ response:
153
+ must_not_contain:
154
+ - "<tool_call>"
155
+ contains_any:
156
+ - "AGENTS.md"
157
+ - "added"
158
+ - "updated"
159
+ - "section"
160
+ - "reflection"
161
+ - "Daily Reflection"
@@ -1,16 +1,16 @@
1
1
  name: "Network: Download File"
2
2
  description: "Tests the http.download tool."
3
3
 
4
- timeout: 180000
4
+ timeout: 20000
5
5
 
6
6
  setup:
7
7
  files: {}
8
8
 
9
- input: "Download the robots.txt from google.com and save it as 'google_robots.txt'."
9
+ input: "Download the robots.txt from https://httpbin.org/robots.txt and save it as 'httpbin_robots.txt'."
10
10
 
11
11
  validate:
12
12
  files:
13
- google_robots.txt:
14
- contains_any: ["User-agent", "Disallow"]
13
+ httpbin_robots.txt:
14
+ contains_any: ["User-agent", "/deny"]
15
15
  response:
16
- contains_any: ["downloaded", "saved", "google_robots.txt"]
16
+ contains_any: ["downloaded", "saved", "httpbin_robots.txt"]
package/src/llm.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { hermesToolMiddleware, xmlToolMiddleware, yamlToolMiddleware } from "@ai-sdk-tool/parser";
2
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
3
+ import { addToolInputExamplesMiddleware, extractReasoningMiddleware, wrapLanguageModel, type LanguageModel, gateway } from "ai";
4
+
5
+ const OPENAI_COMPATIBLE_MODEL_ID = process.env.OPENAI_COMPATIBLE_MODEL_ID ?? 'qwen-local';
6
+ const OPENAI_COMPATIBLE_BASE_URL = process.env.OPENAI_COMPATIBLE_BASE_URL ?? 'http://localhost:8000/v1';
7
+ const AI_GATEWAY_USE_QWEN_MIDDLEWARE = process.env.AI_GATEWAY_USE_QWEN_MIDDLEWARE ?? '';
8
+
9
+ // --- MODEL SETUP ---
10
+ const localProvider = createOpenAICompatible({
11
+ name: 'local',
12
+ baseURL: OPENAI_COMPATIBLE_BASE_URL,
13
+ });
14
+
15
+ export const model : LanguageModel = process.env.AI_GATEWAY_MODEL_ID ? (AI_GATEWAY_USE_QWEN_MIDDLEWARE ? wrapLanguageModel({
16
+ model: gateway(process.env.AI_GATEWAY_MODEL_ID),
17
+ middleware: [
18
+ hermesToolMiddleware,
19
+ //xmlToolMiddleware,
20
+ addToolInputExamplesMiddleware({ prefix: 'Input Examples:', }),
21
+ extractReasoningMiddleware({
22
+ tagName: "think"
23
+ })
24
+ ]
25
+ }) : process.env.AI_GATEWAY_MODEL_ID) : wrapLanguageModel({
26
+ model: localProvider.languageModel(OPENAI_COMPATIBLE_MODEL_ID),
27
+ middleware: [
28
+ hermesToolMiddleware,
29
+ //xmlToolMiddleware,
30
+ addToolInputExamplesMiddleware({ prefix: 'Input Examples:', }),
31
+ extractReasoningMiddleware({
32
+ tagName: "think"
33
+ })
34
+ ]
35
+ });
package/src/logger.ts ADDED
@@ -0,0 +1,39 @@
1
+ import pino from "pino";
2
+
3
+ const isProd = process.env.NODE_ENV === "production";
4
+ const logFile = process.env.LOG_FILE_PATH ?? `${process.cwd()}/logs/clawlet.jsonl`;
5
+
6
+ const transport = pino.transport({
7
+ targets: [
8
+ {
9
+ target: "pino/file",
10
+ level: "debug",
11
+ options: { destination: logFile, mkdir: true }
12
+ },
13
+ {
14
+ target: "pino/file",
15
+ level: "debug",
16
+ options: { destination: 2 }
17
+ }
18
+ ]
19
+ });
20
+
21
+ export const logger = pino({
22
+ level: process.env.LOG_LEVEL ?? (isProd ? "info" : "debug"),
23
+ base: {
24
+ service: process.env.SERVICE_NAME ?? "clawlet",
25
+ env: process.env.NODE_ENV ?? "development",
26
+ version: process.env.APP_VERSION,
27
+ },
28
+ timestamp: () => `,"ts":"${new Date().toISOString()}"`,
29
+ formatters: {
30
+ level(label, number) {
31
+ return { level: number, level_label: label };
32
+ },
33
+ },
34
+ serializers: {
35
+ err: pino.stdSerializers.err,
36
+ },
37
+ },
38
+ transport
39
+ );
package/src/memory.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  import { createStorage, type Storage } from "unstorage";
2
2
  import fsDriver from "unstorage/drivers/fs";
3
- import { type ModelMessage } from "ai";
3
+ import { generateText, type LanguageModel, type ModelMessage } from "ai";
4
4
  import path from "path";
5
- import { LibSqlKeyValueStorage, LibSqlListStorage, SkillHistoryStorage } from "./storage.js";
5
+ import { LibSqlKeyValueStorage, LibSqlListStorage } from "./storage.js";
6
+ import { logger } from './logger.js';
7
+ import memoryDriver from 'unstorage/drivers/memory';
8
+
9
+
10
+ // --- COMPACTION CONFIG ---
11
+ const COMPACT_THRESHOLD = 25; // Trigger compaction when history reaches this many items
12
+ const COMPACT_RANGE = 10; // Number of messages to summarize (items 1..10, skipping system prompt at 0)
6
13
 
7
14
  export class AgentMemory {
8
15
  // 1. Secrets (libSQL - file:secrets.db)
@@ -11,37 +18,98 @@ export class AgentMemory {
11
18
  // 2. History (libSQL - file:history.db)
12
19
  public history: LibSqlListStorage<ModelMessage>;
13
20
 
14
- // 3. Skill History (libSQL - file:history.db, table: skill_history)
15
- public skillHistory: SkillHistoryStorage<ModelMessage>;
16
-
17
- // 4. Workspace (Unstorage - ./workspace)
21
+ // 3. Workspace (Unstorage - ./workspace)
18
22
  public workspace: Storage;
19
23
 
20
- constructor() {
21
- // A. Init Secrets DB
22
- // In Production: process.env.SECRETS_DB_URL (libsql://...)
23
- this.secrets = new LibSqlKeyValueStorage(
24
- process.env.SECRETS_DB_URL || "file:secrets.db",
25
- process.env.SECRETS_AUTH_TOKEN
26
- );
24
+ private constructor(secrets: LibSqlKeyValueStorage, history: LibSqlListStorage<ModelMessage>, workspace: Storage) {
25
+ this.secrets = secrets;
26
+ this.history = history;
27
+ this.workspace = workspace
28
+ }
27
29
 
28
- // B. Init History DB
29
- // In Production: process.env.HISTORY_DB_URL (libsql://...)
30
- this.history = new LibSqlListStorage<ModelMessage>(
31
- process.env.HISTORY_DB_URL || "file:history.db",
32
- process.env.HISTORY_AUTH_TOKEN
30
+ static async createInMemory() {
31
+ return new AgentMemory(
32
+ await LibSqlKeyValueStorage.create(':memory:'),
33
+ await LibSqlListStorage.create<ModelMessage>(':memory:'),
34
+ createStorage({ driver: memoryDriver() })
33
35
  );
36
+ }
34
37
 
35
- // C. Init Skill History (same DB as history, different table)
36
- this.skillHistory = new SkillHistoryStorage<ModelMessage>(
37
- process.env.HISTORY_DB_URL || "file:history.db",
38
- process.env.HISTORY_AUTH_TOKEN
38
+ static async create() {
39
+ return new AgentMemory(
40
+ await LibSqlKeyValueStorage.create(
41
+ process.env.SECRETS_DB_URL || "file:secrets.db",
42
+ process.env.SECRETS_AUTH_TOKEN
43
+ ),
44
+ await LibSqlListStorage.create<ModelMessage>(
45
+ process.env.HISTORY_DB_URL || "file:history.db",
46
+ process.env.HISTORY_AUTH_TOKEN
47
+ ),
48
+ createStorage({
49
+ driver: fsDriver({ base: path.join(process.cwd(), "workspace") })
50
+ })
39
51
  );
52
+ }
53
+
54
+
55
+ /**
56
+ * Compacts history when it reaches COMPACT_THRESHOLD items.
57
+ * Summarizes items 1..COMPACT_RANGE (the system prompt is not included) into a single message
58
+ * using the LLM, then replaces in-memory + persisted history.
59
+ * Result: summary message + remaining messages.
60
+ */
61
+ async compactHistory(name:string, model: LanguageModel): Promise<ModelMessage[]> {
62
+ const messages = await this.history.getAll(name);
63
+ if (messages.length < COMPACT_THRESHOLD) return messages;
64
+
65
+ logger.info({count: messages.length}, `messages to be compacted.`);
66
+
67
+ const toSummarize = messages.slice(0, COMPACT_RANGE);
68
+ const remaining = messages.slice(COMPACT_RANGE);
69
+
70
+ // Build a transcript for the LLM to summarize
71
+ const transcript = toSummarize.map(m => {
72
+ const role = m.role ?? 'unknown';
73
+ const content = typeof m.content === 'string'
74
+ ? m.content
75
+ : JSON.stringify(m.content);
76
+ return `[${role}]: ${content}`;
77
+ }).join('\n\n');
78
+
79
+ try {
80
+ const { text: summary } = await generateText({
81
+ model,
82
+ messages: [
83
+ {
84
+ role: 'system',
85
+ 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.',
86
+ },
87
+ {
88
+ role: 'user',
89
+ content: `Summarize this conversation transcript:\n\n${transcript}`,
90
+ },
91
+ ],
92
+ temperature: 0.3,
93
+ });
94
+
95
+ const summaryMessage: ModelMessage = {
96
+ role: 'assistant',
97
+ content: `[Conversation Summary — compacted ${COMPACT_RANGE} messages]\n\n${summary}`,
98
+ };
99
+
100
+ // Rebuild in-memory messages: summary + remaining
101
+ const compactedMessages = [summaryMessage, ...remaining];
102
+
103
+ // Persist: clear DB and re-write all messages
104
+ await this.history.replaceAll(name, compactedMessages);
40
105
 
41
- // D. Init Workspace (Filesystem)
42
- // Unstorage abstrahiert hier nur das "Wie", aber es bleibt lokal im Ordner.
43
- this.workspace = createStorage({
44
- driver: fsDriver({ base: path.join(process.cwd(), "workspace") })
45
- });
106
+ logger.info({count: compactedMessages.length}, ` Compacted messages.`);
107
+ return compactedMessages;
108
+ } catch (e) {
109
+ logger.error({ err: e }, 'Compaction failed, keeping original history');
110
+ await this.history.replaceAll(name, messages);
111
+ return messages;
112
+ }
46
113
  }
114
+
47
115
  }