pretticlaw 0.1.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.
Files changed (158) hide show
  1. package/CONTRIBUTING.md +123 -0
  2. package/README.md +150 -0
  3. package/assets/logo.png +0 -0
  4. package/dist/agent/context.d.ts +22 -0
  5. package/dist/agent/context.js +85 -0
  6. package/dist/agent/loop.d.ts +63 -0
  7. package/dist/agent/loop.js +244 -0
  8. package/dist/agent/memory.d.ts +16 -0
  9. package/dist/agent/memory.js +98 -0
  10. package/dist/agent/skills.d.ts +18 -0
  11. package/dist/agent/skills.js +121 -0
  12. package/dist/agent/subagent.d.ts +30 -0
  13. package/dist/agent/subagent.js +92 -0
  14. package/dist/agent/tools/base.d.ts +10 -0
  15. package/dist/agent/tools/base.js +58 -0
  16. package/dist/agent/tools/cron.d.ts +43 -0
  17. package/dist/agent/tools/cron.js +83 -0
  18. package/dist/agent/tools/filesystem.d.ts +79 -0
  19. package/dist/agent/tools/filesystem.js +125 -0
  20. package/dist/agent/tools/message.d.ts +41 -0
  21. package/dist/agent/tools/message.js +55 -0
  22. package/dist/agent/tools/registry.d.ts +9 -0
  23. package/dist/agent/tools/registry.js +33 -0
  24. package/dist/agent/tools/shell.d.ts +26 -0
  25. package/dist/agent/tools/shell.js +78 -0
  26. package/dist/agent/tools/spawn.d.ts +27 -0
  27. package/dist/agent/tools/spawn.js +35 -0
  28. package/dist/agent/tools/web.d.ts +50 -0
  29. package/dist/agent/tools/web.js +119 -0
  30. package/dist/bus/async-queue.d.ts +7 -0
  31. package/dist/bus/async-queue.js +20 -0
  32. package/dist/bus/events.d.ts +19 -0
  33. package/dist/bus/events.js +3 -0
  34. package/dist/bus/queue.d.ts +12 -0
  35. package/dist/bus/queue.js +23 -0
  36. package/dist/channels/base.d.ts +22 -0
  37. package/dist/channels/base.js +35 -0
  38. package/dist/channels/discord.d.ts +24 -0
  39. package/dist/channels/discord.js +133 -0
  40. package/dist/channels/manager.d.ts +17 -0
  41. package/dist/channels/manager.js +67 -0
  42. package/dist/channels/stub.d.ts +10 -0
  43. package/dist/channels/stub.js +18 -0
  44. package/dist/channels/telegram.d.ts +20 -0
  45. package/dist/channels/telegram.js +93 -0
  46. package/dist/cli/commands.d.ts +2 -0
  47. package/dist/cli/commands.js +552 -0
  48. package/dist/config/loader.d.ts +5 -0
  49. package/dist/config/loader.js +55 -0
  50. package/dist/config/schema.d.ts +246 -0
  51. package/dist/config/schema.js +94 -0
  52. package/dist/cron/service.d.ts +33 -0
  53. package/dist/cron/service.js +195 -0
  54. package/dist/cron/types.d.ts +47 -0
  55. package/dist/cron/types.js +1 -0
  56. package/dist/dashboard/index.html +1567 -0
  57. package/dist/heartbeat/service.d.ts +21 -0
  58. package/dist/heartbeat/service.js +101 -0
  59. package/dist/index.d.ts +2 -0
  60. package/dist/index.js +5 -0
  61. package/dist/providers/base.d.ts +23 -0
  62. package/dist/providers/base.js +21 -0
  63. package/dist/providers/custom-provider.d.ts +16 -0
  64. package/dist/providers/custom-provider.js +49 -0
  65. package/dist/providers/litellm-provider.d.ts +19 -0
  66. package/dist/providers/litellm-provider.js +128 -0
  67. package/dist/providers/registry.d.ts +5 -0
  68. package/dist/providers/registry.js +45 -0
  69. package/dist/session/manager.d.ts +31 -0
  70. package/dist/session/manager.js +116 -0
  71. package/dist/skills/README.md +25 -0
  72. package/dist/skills/clawhub/SKILL.md +53 -0
  73. package/dist/skills/cron/SKILL.md +57 -0
  74. package/dist/skills/github/SKILL.md +48 -0
  75. package/dist/skills/memory/SKILL.md +31 -0
  76. package/dist/skills/skill-creator/SKILL.md +371 -0
  77. package/dist/skills/summarize/SKILL.md +67 -0
  78. package/dist/skills/tmux/SKILL.md +121 -0
  79. package/dist/skills/tmux/scripts/find-sessions.sh +112 -0
  80. package/dist/skills/tmux/scripts/wait-for-text.sh +83 -0
  81. package/dist/skills/weather/SKILL.md +49 -0
  82. package/dist/templates/AGENTS.md +23 -0
  83. package/dist/templates/HEARTBEAT.md +16 -0
  84. package/dist/templates/SOUL.md +21 -0
  85. package/dist/templates/TOOLS.md +15 -0
  86. package/dist/templates/USER.md +49 -0
  87. package/dist/templates/memory/MEMORY.md +23 -0
  88. package/dist/types.d.ts +4 -0
  89. package/dist/types.js +3 -0
  90. package/dist/utils/helpers.d.ts +5 -0
  91. package/dist/utils/helpers.js +53 -0
  92. package/dist/web/server.d.ts +15 -0
  93. package/dist/web/server.js +169 -0
  94. package/package.json +37 -0
  95. package/scripts/copy-assets.mjs +21 -0
  96. package/src/agent/context.ts +90 -0
  97. package/src/agent/loop.ts +291 -0
  98. package/src/agent/memory.ts +104 -0
  99. package/src/agent/skills.ts +121 -0
  100. package/src/agent/subagent.ts +96 -0
  101. package/src/agent/tools/base.ts +59 -0
  102. package/src/agent/tools/cron.ts +79 -0
  103. package/src/agent/tools/filesystem.ts +93 -0
  104. package/src/agent/tools/message.ts +57 -0
  105. package/src/agent/tools/registry.ts +36 -0
  106. package/src/agent/tools/shell.ts +69 -0
  107. package/src/agent/tools/spawn.ts +37 -0
  108. package/src/agent/tools/web.ts +108 -0
  109. package/src/bus/async-queue.ts +20 -0
  110. package/src/bus/events.ts +23 -0
  111. package/src/bus/queue.ts +31 -0
  112. package/src/channels/base.ts +36 -0
  113. package/src/channels/discord.ts +156 -0
  114. package/src/channels/manager.ts +70 -0
  115. package/src/channels/stub.ts +20 -0
  116. package/src/channels/telegram.ts +120 -0
  117. package/src/cli/commands.ts +581 -0
  118. package/src/config/loader.ts +58 -0
  119. package/src/config/schema.ts +144 -0
  120. package/src/cron/service.ts +190 -0
  121. package/src/cron/types.ts +36 -0
  122. package/src/dashboard/index.html +1567 -0
  123. package/src/heartbeat/service.ts +95 -0
  124. package/src/index.ts +6 -0
  125. package/src/providers/base.ts +43 -0
  126. package/src/providers/custom-provider.ts +46 -0
  127. package/src/providers/litellm-provider.ts +131 -0
  128. package/src/providers/registry.ts +48 -0
  129. package/src/session/manager.ts +129 -0
  130. package/src/skills/README.md +25 -0
  131. package/src/skills/clawhub/SKILL.md +53 -0
  132. package/src/skills/cron/SKILL.md +57 -0
  133. package/src/skills/github/SKILL.md +48 -0
  134. package/src/skills/memory/SKILL.md +31 -0
  135. package/src/skills/skill-creator/SKILL.md +371 -0
  136. package/src/skills/summarize/SKILL.md +67 -0
  137. package/src/skills/tmux/SKILL.md +121 -0
  138. package/src/skills/tmux/scripts/find-sessions.sh +112 -0
  139. package/src/skills/tmux/scripts/wait-for-text.sh +83 -0
  140. package/src/skills/weather/SKILL.md +49 -0
  141. package/src/templates/AGENTS.md +23 -0
  142. package/src/templates/HEARTBEAT.md +16 -0
  143. package/src/templates/SOUL.md +21 -0
  144. package/src/templates/TOOLS.md +15 -0
  145. package/src/templates/USER.md +49 -0
  146. package/src/templates/memory/MEMORY.md +23 -0
  147. package/src/types/prompts.d.ts +14 -0
  148. package/src/types/ws.d.ts +15 -0
  149. package/src/types.ts +5 -0
  150. package/src/utils/helpers.ts +55 -0
  151. package/src/web/server.ts +198 -0
  152. package/test/context.test.ts +27 -0
  153. package/test/cron-service.test.ts +31 -0
  154. package/test/message-tool.test.ts +10 -0
  155. package/test/providers.test.ts +43 -0
  156. package/test/tool-validation.test.ts +61 -0
  157. package/tsconfig.json +16 -0
  158. package/vitest.config.ts +8 -0
@@ -0,0 +1,244 @@
1
+ import path from "node:path";
2
+ import { ContextBuilder } from "./context.js";
3
+ import { MemoryStore } from "./memory.js";
4
+ import { SubagentManager } from "./subagent.js";
5
+ import { ReadFileTool, WriteFileTool, EditFileTool, ListDirTool } from "./tools/filesystem.js";
6
+ import { ExecTool } from "./tools/shell.js";
7
+ import { WebSearchTool, WebFetchTool } from "./tools/web.js";
8
+ import { MessageTool } from "./tools/message.js";
9
+ import { SpawnTool } from "./tools/spawn.js";
10
+ import { CronTool } from "./tools/cron.js";
11
+ import { ToolRegistry } from "./tools/registry.js";
12
+ import { sessionKey as getSessionKey } from "../bus/events.js";
13
+ import { SessionManager } from "../session/manager.js";
14
+ function stripThink(text) {
15
+ if (!text)
16
+ return null;
17
+ return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim() || null;
18
+ }
19
+ export class AgentLoop {
20
+ input;
21
+ context;
22
+ sessions;
23
+ tools;
24
+ subagents;
25
+ running = false;
26
+ consolidating = new Set();
27
+ activeTasks = new Map();
28
+ processing = Promise.resolve();
29
+ static TOOL_RESULT_MAX_CHARS = 500;
30
+ constructor(input) {
31
+ this.input = input;
32
+ const workspace = path.resolve(input.workspace);
33
+ this.context = new ContextBuilder(workspace);
34
+ this.sessions = input.sessionManager ?? new SessionManager(workspace);
35
+ this.tools = new ToolRegistry();
36
+ const model = input.model ?? input.provider.getDefaultModel();
37
+ this.subagents = new SubagentManager(input.provider, workspace, input.bus, model, input.temperature ?? 0.1, input.maxTokens ?? 4096, input.braveApiKey ?? null, input.execConfig ?? { timeout: 60, pathAppend: "" }, !!input.restrictToWorkspace);
38
+ this.registerDefaultTools();
39
+ }
40
+ get model() {
41
+ return this.input.model ?? this.input.provider.getDefaultModel();
42
+ }
43
+ get channelsConfig() {
44
+ return this.input.channelsConfig;
45
+ }
46
+ registerDefaultTools() {
47
+ const workspace = path.resolve(this.input.workspace);
48
+ const allowed = this.input.restrictToWorkspace ? workspace : undefined;
49
+ this.tools.register(new ReadFileTool(workspace, allowed));
50
+ this.tools.register(new WriteFileTool(workspace, allowed));
51
+ this.tools.register(new EditFileTool(workspace, allowed));
52
+ this.tools.register(new ListDirTool(workspace, allowed));
53
+ this.tools.register(new ExecTool(this.input.execConfig?.timeout ?? 60, workspace, !!this.input.restrictToWorkspace, this.input.execConfig?.pathAppend ?? ""));
54
+ this.tools.register(new WebSearchTool(this.input.braveApiKey ?? null));
55
+ this.tools.register(new WebFetchTool());
56
+ this.tools.register(new MessageTool((msg) => this.input.bus.publishOutbound(msg)));
57
+ this.tools.register(new SpawnTool(this.subagents));
58
+ if (this.input.cronService)
59
+ this.tools.register(new CronTool(this.input.cronService));
60
+ }
61
+ setToolContext(channel, chatId, messageId) {
62
+ const message = this.tools.get("message");
63
+ if (message && message instanceof MessageTool)
64
+ message.setContext(channel, chatId, messageId);
65
+ const spawn = this.tools.get("spawn");
66
+ if (spawn?.setContext)
67
+ spawn.setContext(channel, chatId);
68
+ const cron = this.tools.get("cron");
69
+ if (cron?.setContext)
70
+ cron.setContext(channel, chatId);
71
+ }
72
+ toolHint(toolCalls) {
73
+ return toolCalls.map((tc) => {
74
+ const val = Object.values(tc.arguments ?? {})[0];
75
+ if (typeof val !== "string")
76
+ return tc.name;
77
+ return val.length > 40 ? `${tc.name}("${val.slice(0, 40)}...")` : `${tc.name}("${val}")`;
78
+ }).join(", ");
79
+ }
80
+ async runAgentLoop(initialMessages, onProgress) {
81
+ const messages = [...initialMessages];
82
+ let finalContent = null;
83
+ const toolsUsed = [];
84
+ const maxIterations = this.input.maxIterations ?? 40;
85
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
86
+ const response = await this.input.provider.chat({
87
+ messages,
88
+ tools: this.tools.getDefinitions(),
89
+ model: this.model,
90
+ temperature: this.input.temperature ?? 0.1,
91
+ maxTokens: this.input.maxTokens ?? 4096,
92
+ });
93
+ if (response.toolCalls.length) {
94
+ if (onProgress) {
95
+ const clean = stripThink(response.content);
96
+ if (clean)
97
+ await onProgress(clean, { toolHint: false });
98
+ await onProgress(this.toolHint(response.toolCalls), { toolHint: true });
99
+ }
100
+ const toolCallDicts = response.toolCalls.map((tc) => ({ id: tc.id, type: "function", function: { name: tc.name, arguments: JSON.stringify(tc.arguments) } }));
101
+ this.context.addAssistantMessage(messages, response.content, toolCallDicts, response.reasoningContent);
102
+ for (const call of response.toolCalls) {
103
+ toolsUsed.push(call.name);
104
+ const result = await this.tools.execute(call.name, call.arguments);
105
+ this.context.addToolResult(messages, call.id, call.name, result);
106
+ }
107
+ }
108
+ else {
109
+ finalContent = stripThink(response.content);
110
+ this.context.addAssistantMessage(messages, finalContent, undefined, response.reasoningContent);
111
+ break;
112
+ }
113
+ }
114
+ if (finalContent == null) {
115
+ finalContent = `I reached the maximum number of tool call iterations (${maxIterations}) without completing the task.`;
116
+ }
117
+ return { finalContent, toolsUsed, messages };
118
+ }
119
+ async run() {
120
+ this.running = true;
121
+ while (this.running) {
122
+ const msg = await this.input.bus.consumeInbound();
123
+ if (msg.content.trim().toLowerCase() === "/stop") {
124
+ await this.handleStop(msg);
125
+ continue;
126
+ }
127
+ const session = getSessionKey(msg);
128
+ const task = this.dispatch(msg).finally(() => {
129
+ const set = this.activeTasks.get(session);
130
+ if (!set)
131
+ return;
132
+ set.delete(task);
133
+ if (!set.size)
134
+ this.activeTasks.delete(session);
135
+ });
136
+ if (!this.activeTasks.has(session))
137
+ this.activeTasks.set(session, new Set());
138
+ this.activeTasks.get(session).add(task);
139
+ }
140
+ }
141
+ stop() {
142
+ this.running = false;
143
+ }
144
+ async processDirect(content, session = "cli:direct", channel = "cli", chatId = "direct", onProgress) {
145
+ const msg = { channel, senderId: "user", chatId, content };
146
+ const out = await this.processMessage(msg, session, onProgress);
147
+ return out?.content ?? "";
148
+ }
149
+ async dispatch(msg) {
150
+ this.processing = this.processing.then(async () => {
151
+ try {
152
+ const response = await this.processMessage(msg);
153
+ if (response) {
154
+ await this.input.bus.publishOutbound(response);
155
+ }
156
+ else if (msg.channel === "cli") {
157
+ await this.input.bus.publishOutbound({ channel: msg.channel, chatId: msg.chatId, content: "", metadata: msg.metadata ?? {} });
158
+ }
159
+ }
160
+ catch {
161
+ await this.input.bus.publishOutbound({ channel: msg.channel, chatId: msg.chatId, content: "Sorry, I encountered an error." });
162
+ }
163
+ });
164
+ return this.processing;
165
+ }
166
+ async handleStop(msg) {
167
+ const key = getSessionKey(msg);
168
+ const tasks = [...(this.activeTasks.get(key) ?? [])];
169
+ const subCancelled = await this.subagents.cancelBySession(key);
170
+ const total = tasks.length + subCancelled;
171
+ await this.input.bus.publishOutbound({ channel: msg.channel, chatId: msg.chatId, content: total ? `Stopped ${total} task(s).` : "No active task to stop." });
172
+ }
173
+ async consolidateMemory(session, archiveAll = false) {
174
+ return new MemoryStore(this.input.workspace).consolidate(session, this.input.provider, this.model, { archiveAll, memoryWindow: this.input.memoryWindow ?? 100 });
175
+ }
176
+ saveTurn(session, messages, skip) {
177
+ for (const m of messages.slice(skip)) {
178
+ const entry = { ...m };
179
+ delete entry.reasoning_content;
180
+ if (entry.role === "tool" && typeof entry.content === "string" && entry.content.length > AgentLoop.TOOL_RESULT_MAX_CHARS) {
181
+ entry.content = `${entry.content.slice(0, AgentLoop.TOOL_RESULT_MAX_CHARS)}\n... (truncated)`;
182
+ }
183
+ if (entry.role === "user" && typeof entry.content === "string" && entry.content.startsWith(ContextBuilder.RUNTIME_CONTEXT_TAG))
184
+ continue;
185
+ if (!entry.timestamp)
186
+ entry.timestamp = new Date().toISOString();
187
+ session.messages.push(entry);
188
+ }
189
+ session.updatedAt = new Date().toISOString();
190
+ }
191
+ async processMessage(msg, sessionKeyOverride, onProgress) {
192
+ if (msg.channel === "system") {
193
+ const [channel, chatId] = String(msg.chatId).includes(":") ? String(msg.chatId).split(/:(.*)/s, 2) : ["cli", String(msg.chatId)];
194
+ const key = `${channel}:${chatId}`;
195
+ const session = this.sessions.getOrCreate(key);
196
+ this.setToolContext(channel, chatId, msg.metadata?.message_id);
197
+ const history = session.getHistory(this.input.memoryWindow ?? 100);
198
+ const messages = this.context.buildMessages({ history, currentMessage: msg.content, channel, chatId });
199
+ const { finalContent, messages: allMsgs } = await this.runAgentLoop(messages);
200
+ this.saveTurn(session, allMsgs, 1 + history.length);
201
+ this.sessions.save(session);
202
+ return { channel, chatId, content: finalContent ?? "Background task completed." };
203
+ }
204
+ const key = sessionKeyOverride ?? getSessionKey(msg);
205
+ const session = this.sessions.getOrCreate(key);
206
+ const cmd = msg.content.trim().toLowerCase();
207
+ if (cmd === "/new") {
208
+ if (session.messages.length) {
209
+ const ok = await this.consolidateMemory(session, true);
210
+ if (!ok)
211
+ return { channel: msg.channel, chatId: msg.chatId, content: "Memory archival failed, session not cleared. Please try again." };
212
+ }
213
+ session.clear();
214
+ this.sessions.save(session);
215
+ this.sessions.invalidate(session.key);
216
+ return { channel: msg.channel, chatId: msg.chatId, content: "New session started." };
217
+ }
218
+ if (cmd === "/help") {
219
+ return { channel: msg.channel, chatId: msg.chatId, content: "pretticlaw commands:\n/new - Start a new conversation\n/stop - Stop the current task\n/help - Show available commands" };
220
+ }
221
+ const unconsolidated = session.messages.length - session.lastConsolidated;
222
+ if (unconsolidated >= (this.input.memoryWindow ?? 100) && !this.consolidating.has(session.key)) {
223
+ this.consolidating.add(session.key);
224
+ void this.consolidateMemory(session).finally(() => this.consolidating.delete(session.key));
225
+ }
226
+ this.setToolContext(msg.channel, msg.chatId, msg.metadata?.message_id);
227
+ const messageTool = this.tools.get("message");
228
+ if (messageTool instanceof MessageTool)
229
+ messageTool.startTurn();
230
+ const history = session.getHistory(this.input.memoryWindow ?? 100);
231
+ const initialMessages = this.context.buildMessages({ history, currentMessage: msg.content, media: msg.media, channel: msg.channel, chatId: msg.chatId });
232
+ const busProgress = async (content, meta) => {
233
+ const metadata = { ...(msg.metadata ?? {}), _progress: true, _tool_hint: !!meta?.toolHint };
234
+ await this.input.bus.publishOutbound({ channel: msg.channel, chatId: msg.chatId, content, metadata });
235
+ };
236
+ const { finalContent, messages: allMsgs } = await this.runAgentLoop(initialMessages, onProgress ?? busProgress);
237
+ const out = finalContent ?? "I've completed processing but have no response to give.";
238
+ this.saveTurn(session, allMsgs, 1 + history.length);
239
+ this.sessions.save(session);
240
+ if (messageTool instanceof MessageTool && messageTool.sentInTurn)
241
+ return null;
242
+ return { channel: msg.channel, chatId: msg.chatId, content: out, metadata: msg.metadata ?? {} };
243
+ }
244
+ }
@@ -0,0 +1,16 @@
1
+ import type { LLMProvider } from "../providers/base.js";
2
+ import type { Session } from "../session/manager.js";
3
+ export declare class MemoryStore {
4
+ readonly memoryDir: string;
5
+ readonly memoryFile: string;
6
+ readonly historyFile: string;
7
+ constructor(workspace: string);
8
+ readLongTerm(): string;
9
+ writeLongTerm(content: string): void;
10
+ appendHistory(entry: string): void;
11
+ getMemoryContext(): string;
12
+ consolidate(session: Session, provider: LLMProvider, model: string, opts?: {
13
+ archiveAll?: boolean;
14
+ memoryWindow?: number;
15
+ }): Promise<boolean>;
16
+ }
@@ -0,0 +1,98 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const SAVE_MEMORY_TOOL = [{
4
+ type: "function",
5
+ function: {
6
+ name: "save_memory",
7
+ description: "Save the memory consolidation result to persistent storage.",
8
+ parameters: {
9
+ type: "object",
10
+ properties: {
11
+ history_entry: { type: "string" },
12
+ memory_update: { type: "string" },
13
+ },
14
+ required: ["history_entry", "memory_update"],
15
+ },
16
+ },
17
+ }];
18
+ export class MemoryStore {
19
+ memoryDir;
20
+ memoryFile;
21
+ historyFile;
22
+ constructor(workspace) {
23
+ this.memoryDir = path.join(workspace, "memory");
24
+ fs.mkdirSync(this.memoryDir, { recursive: true });
25
+ this.memoryFile = path.join(this.memoryDir, "MEMORY.md");
26
+ this.historyFile = path.join(this.memoryDir, "HISTORY.md");
27
+ }
28
+ readLongTerm() {
29
+ return fs.existsSync(this.memoryFile) ? fs.readFileSync(this.memoryFile, "utf8") : "";
30
+ }
31
+ writeLongTerm(content) {
32
+ fs.writeFileSync(this.memoryFile, content, "utf8");
33
+ }
34
+ appendHistory(entry) {
35
+ fs.appendFileSync(this.historyFile, `${entry.trim()}\n\n`, "utf8");
36
+ }
37
+ getMemoryContext() {
38
+ const longTerm = this.readLongTerm();
39
+ return longTerm ? `## Long-term Memory\n${longTerm}` : "";
40
+ }
41
+ async consolidate(session, provider, model, opts) {
42
+ const archiveAll = opts?.archiveAll ?? false;
43
+ const memoryWindow = opts?.memoryWindow ?? 50;
44
+ let oldMessages = [];
45
+ let keepCount = 0;
46
+ if (archiveAll) {
47
+ oldMessages = session.messages;
48
+ keepCount = 0;
49
+ }
50
+ else {
51
+ keepCount = Math.floor(memoryWindow / 2);
52
+ if (session.messages.length <= keepCount)
53
+ return true;
54
+ if (session.messages.length - session.lastConsolidated <= 0)
55
+ return true;
56
+ oldMessages = session.messages.slice(session.lastConsolidated, session.messages.length - keepCount);
57
+ if (!oldMessages.length)
58
+ return true;
59
+ }
60
+ const lines = [];
61
+ for (const m of oldMessages) {
62
+ if (!m.content)
63
+ continue;
64
+ const tools = Array.isArray(m.tools_used) && m.tools_used.length ? ` [tools: ${m.tools_used.join(", ")}]` : "";
65
+ lines.push(`[${String(m.timestamp ?? "?").slice(0, 16)}] ${String(m.role).toUpperCase()}${tools}: ${String(m.content)}`);
66
+ }
67
+ const currentMemory = this.readLongTerm();
68
+ const prompt = `Process this conversation and call the save_memory tool with your consolidation.\n\n## Current Long-term Memory\n${currentMemory || "(empty)"}\n\n## Conversation to Process\n${lines.join("\n")}`;
69
+ try {
70
+ const response = await provider.chat({
71
+ model,
72
+ messages: [
73
+ { role: "system", content: "You are a memory consolidation agent. Call the save_memory tool." },
74
+ { role: "user", content: prompt },
75
+ ],
76
+ tools: SAVE_MEMORY_TOOL,
77
+ });
78
+ if (!response.toolCalls.length)
79
+ return false;
80
+ const call = response.toolCalls[0];
81
+ const args = call.arguments ?? {};
82
+ const entry = args.history_entry;
83
+ const update = args.memory_update;
84
+ if (entry != null)
85
+ this.appendHistory(typeof entry === "string" ? entry : JSON.stringify(entry));
86
+ if (update != null) {
87
+ const text = typeof update === "string" ? update : JSON.stringify(update);
88
+ if (text !== currentMemory)
89
+ this.writeLongTerm(text);
90
+ }
91
+ session.lastConsolidated = archiveAll ? 0 : session.messages.length - keepCount;
92
+ return true;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,18 @@
1
+ export declare class SkillsLoader {
2
+ private readonly workspaceSkills;
3
+ private readonly builtinSkills;
4
+ constructor(workspace: string, builtinSkillsDir?: string);
5
+ listSkills(filterUnavailable?: boolean): Array<{
6
+ name: string;
7
+ path: string;
8
+ source: string;
9
+ }>;
10
+ loadSkill(name: string): string | null;
11
+ loadSkillsForContext(skillNames: string[]): string;
12
+ buildSkillsSummary(): string;
13
+ getAlwaysSkills(): string[];
14
+ getSkillMetadata(name: string): Record<string, unknown> | null;
15
+ private stripFrontmatter;
16
+ private getSkillMeta;
17
+ private checkRequirements;
18
+ }
@@ -0,0 +1,121 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const BUILTIN_SKILLS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../skills");
5
+ export class SkillsLoader {
6
+ workspaceSkills;
7
+ builtinSkills;
8
+ constructor(workspace, builtinSkillsDir = BUILTIN_SKILLS_DIR) {
9
+ this.workspaceSkills = path.join(workspace, "skills");
10
+ this.builtinSkills = builtinSkillsDir;
11
+ }
12
+ listSkills(filterUnavailable = true) {
13
+ const skills = [];
14
+ const load = (root, source) => {
15
+ if (!fs.existsSync(root))
16
+ return;
17
+ for (const name of fs.readdirSync(root)) {
18
+ const skillFile = path.join(root, name, "SKILL.md");
19
+ if (!fs.existsSync(skillFile))
20
+ continue;
21
+ if (skills.some((s) => s.name === name))
22
+ continue;
23
+ skills.push({ name, path: skillFile, source });
24
+ }
25
+ };
26
+ load(this.workspaceSkills, "workspace");
27
+ load(this.builtinSkills, "builtin");
28
+ if (!filterUnavailable)
29
+ return skills;
30
+ return skills.filter((s) => this.checkRequirements(this.getSkillMeta(s.name)));
31
+ }
32
+ loadSkill(name) {
33
+ const workspace = path.join(this.workspaceSkills, name, "SKILL.md");
34
+ if (fs.existsSync(workspace))
35
+ return fs.readFileSync(workspace, "utf8");
36
+ const builtin = path.join(this.builtinSkills, name, "SKILL.md");
37
+ if (fs.existsSync(builtin))
38
+ return fs.readFileSync(builtin, "utf8");
39
+ return null;
40
+ }
41
+ loadSkillsForContext(skillNames) {
42
+ const parts = [];
43
+ for (const name of skillNames) {
44
+ const c = this.loadSkill(name);
45
+ if (c)
46
+ parts.push(`### Skill: ${name}\n\n${this.stripFrontmatter(c)}`);
47
+ }
48
+ return parts.join("\n\n---\n\n");
49
+ }
50
+ buildSkillsSummary() {
51
+ const all = this.listSkills(false);
52
+ if (!all.length)
53
+ return "";
54
+ const esc = (s) => s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
55
+ const lines = ["<skills>"];
56
+ for (const s of all) {
57
+ const meta = this.getSkillMetadata(s.name) ?? {};
58
+ const prettiMeta = this.getSkillMeta(s.name);
59
+ const available = this.checkRequirements(prettiMeta);
60
+ lines.push(` <skill available="${available}">`);
61
+ lines.push(` <name>${esc(s.name)}</name>`);
62
+ lines.push(` <description>${esc(meta.description || s.name)}</description>`);
63
+ lines.push(` <location>${s.path}</location>`);
64
+ lines.push(" </skill>");
65
+ }
66
+ lines.push("</skills>");
67
+ return lines.join("\n");
68
+ }
69
+ getAlwaysSkills() {
70
+ const out = [];
71
+ for (const s of this.listSkills(true)) {
72
+ const meta = this.getSkillMetadata(s.name) ?? {};
73
+ const m = this.getSkillMeta(s.name);
74
+ if (m.always || meta.always)
75
+ out.push(s.name);
76
+ }
77
+ return out;
78
+ }
79
+ getSkillMetadata(name) {
80
+ const content = this.loadSkill(name);
81
+ if (!content?.startsWith("---"))
82
+ return null;
83
+ const m = content.match(/^---\n([\s\S]*?)\n---/);
84
+ if (!m)
85
+ return null;
86
+ const out = {};
87
+ for (const line of m[1].split(/\r?\n/)) {
88
+ const idx = line.indexOf(":");
89
+ if (idx <= 0)
90
+ continue;
91
+ out[line.slice(0, idx).trim()] = line.slice(idx + 1).trim().replace(/^['"]|['"]$/g, "");
92
+ }
93
+ return out;
94
+ }
95
+ stripFrontmatter(content) {
96
+ const m = content.match(/^---\n[\s\S]*?\n---\n?/);
97
+ return m ? content.slice(m[0].length).trim() : content;
98
+ }
99
+ getSkillMeta(name) {
100
+ const meta = this.getSkillMetadata(name) ?? {};
101
+ const raw = typeof meta.metadata === "string" ? meta.metadata : "{}";
102
+ try {
103
+ const data = JSON.parse(raw);
104
+ return data.pretticlaw || data.openclaw || {};
105
+ }
106
+ catch {
107
+ return {};
108
+ }
109
+ }
110
+ checkRequirements(skillMeta) {
111
+ const req = skillMeta.requires ?? {};
112
+ const bins = req.bins ?? [];
113
+ const env = req.env ?? [];
114
+ const hasBin = (name) => {
115
+ const pathEntries = (process.env.PATH ?? "").split(path.delimiter);
116
+ const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
117
+ return pathEntries.some((p) => exts.some((e) => fs.existsSync(path.join(p, `${name}${e}`))));
118
+ };
119
+ return bins.every(hasBin) && env.every((k) => !!process.env[k]);
120
+ }
121
+ }
@@ -0,0 +1,30 @@
1
+ import type { MessageBus } from "../bus/queue.js";
2
+ import type { LLMProvider } from "../providers/base.js";
3
+ export declare class SubagentManager {
4
+ private readonly provider;
5
+ private readonly workspace;
6
+ private readonly bus;
7
+ private readonly model;
8
+ private readonly temperature;
9
+ private readonly maxTokens;
10
+ private readonly braveApiKey;
11
+ private readonly execConfig;
12
+ private readonly restrictToWorkspace;
13
+ private running;
14
+ private sessionTasks;
15
+ constructor(provider: LLMProvider, workspace: string, bus: MessageBus, model: string, temperature: number, maxTokens: number, braveApiKey: string | null, execConfig: {
16
+ timeout: number;
17
+ pathAppend: string;
18
+ }, restrictToWorkspace: boolean);
19
+ spawn(input: {
20
+ task: string;
21
+ label: string | null;
22
+ originChannel: string;
23
+ originChatId: string;
24
+ sessionKey: string;
25
+ }): Promise<string>;
26
+ private runSubagent;
27
+ private buildSubagentPrompt;
28
+ cancelBySession(sessionKey: string): Promise<number>;
29
+ getRunningCount(): number;
30
+ }
@@ -0,0 +1,92 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { ToolRegistry } from "./tools/registry.js";
3
+ import { ReadFileTool, WriteFileTool, EditFileTool, ListDirTool } from "./tools/filesystem.js";
4
+ import { ExecTool } from "./tools/shell.js";
5
+ import { WebSearchTool, WebFetchTool } from "./tools/web.js";
6
+ export class SubagentManager {
7
+ provider;
8
+ workspace;
9
+ bus;
10
+ model;
11
+ temperature;
12
+ maxTokens;
13
+ braveApiKey;
14
+ execConfig;
15
+ restrictToWorkspace;
16
+ running = new Map();
17
+ sessionTasks = new Map();
18
+ constructor(provider, workspace, bus, model, temperature, maxTokens, braveApiKey, execConfig, restrictToWorkspace) {
19
+ this.provider = provider;
20
+ this.workspace = workspace;
21
+ this.bus = bus;
22
+ this.model = model;
23
+ this.temperature = temperature;
24
+ this.maxTokens = maxTokens;
25
+ this.braveApiKey = braveApiKey;
26
+ this.execConfig = execConfig;
27
+ this.restrictToWorkspace = restrictToWorkspace;
28
+ }
29
+ async spawn(input) {
30
+ const taskId = randomUUID().slice(0, 8);
31
+ const label = input.label ?? (input.task.length > 30 ? `${input.task.slice(0, 30)}...` : input.task);
32
+ const p = this.runSubagent(taskId, input.task, label, { channel: input.originChannel, chatId: input.originChatId })
33
+ .finally(() => {
34
+ this.running.delete(taskId);
35
+ const ids = this.sessionTasks.get(input.sessionKey);
36
+ if (ids) {
37
+ ids.delete(taskId);
38
+ if (!ids.size)
39
+ this.sessionTasks.delete(input.sessionKey);
40
+ }
41
+ });
42
+ this.running.set(taskId, p);
43
+ if (!this.sessionTasks.has(input.sessionKey))
44
+ this.sessionTasks.set(input.sessionKey, new Set());
45
+ this.sessionTasks.get(input.sessionKey).add(taskId);
46
+ return `Subagent [${label}] started (id: ${taskId}). I'll notify you when it completes.`;
47
+ }
48
+ async runSubagent(taskId, task, label, origin) {
49
+ const tools = new ToolRegistry();
50
+ const allowedDir = this.restrictToWorkspace ? this.workspace : undefined;
51
+ tools.register(new ReadFileTool(this.workspace, allowedDir));
52
+ tools.register(new WriteFileTool(this.workspace, allowedDir));
53
+ tools.register(new EditFileTool(this.workspace, allowedDir));
54
+ tools.register(new ListDirTool(this.workspace, allowedDir));
55
+ tools.register(new ExecTool(this.execConfig.timeout, this.workspace, this.restrictToWorkspace, this.execConfig.pathAppend));
56
+ tools.register(new WebSearchTool(this.braveApiKey));
57
+ tools.register(new WebFetchTool());
58
+ let messages = [
59
+ { role: "system", content: this.buildSubagentPrompt() },
60
+ { role: "user", content: task },
61
+ ];
62
+ let finalResult = "Task completed but no final response was generated.";
63
+ for (let i = 0; i < 15; i++) {
64
+ const response = await this.provider.chat({ messages, tools: tools.getDefinitions(), model: this.model, temperature: this.temperature, maxTokens: this.maxTokens });
65
+ if (response.toolCalls.length) {
66
+ messages.push({ role: "assistant", content: response.content ?? "", tool_calls: response.toolCalls.map((tc) => ({ id: tc.id, type: "function", function: { name: tc.name, arguments: JSON.stringify(tc.arguments) } })) });
67
+ for (const tc of response.toolCalls) {
68
+ const result = await tools.execute(tc.name, tc.arguments);
69
+ messages.push({ role: "tool", tool_call_id: tc.id, name: tc.name, content: result });
70
+ }
71
+ }
72
+ else {
73
+ finalResult = response.content ?? finalResult;
74
+ break;
75
+ }
76
+ }
77
+ const announceContent = `[Subagent '${label}' completed successfully]\n\nTask: ${task}\n\nResult:\n${finalResult}\n\nSummarize this naturally for the user. Keep it brief (1-2 sentences).`;
78
+ const msg = { channel: "system", senderId: "subagent", chatId: `${origin.channel}:${origin.chatId}`, content: announceContent };
79
+ await this.bus.publishInbound(msg);
80
+ }
81
+ buildSubagentPrompt() {
82
+ const now = new Date().toLocaleString();
83
+ return `# Subagent\n\nCurrent Time: ${now}\n\nYou are a subagent spawned by the main agent to complete a specific task. Stay focused and concise.`;
84
+ }
85
+ async cancelBySession(sessionKey) {
86
+ const ids = [...(this.sessionTasks.get(sessionKey) ?? [])];
87
+ return ids.filter((id) => this.running.has(id)).length;
88
+ }
89
+ getRunningCount() {
90
+ return this.running.size;
91
+ }
92
+ }
@@ -0,0 +1,10 @@
1
+ export declare abstract class Tool {
2
+ protected static typeMap: Record<string, (v: unknown) => boolean>;
3
+ abstract readonly name: string;
4
+ abstract readonly description: string;
5
+ abstract readonly parameters: Record<string, unknown>;
6
+ abstract execute(args: Record<string, unknown>): Promise<string>;
7
+ validateParams(params: Record<string, unknown>): string[];
8
+ private validateValue;
9
+ toSchema(): Record<string, unknown>;
10
+ }