openvole 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.
- package/LICENSE +21 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3250 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +676 -0
- package/dist/index.js +2242 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2242 @@
|
|
|
1
|
+
// src/config/index.ts
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
var DEFAULT_LOOP_CONFIG = {
|
|
5
|
+
maxIterations: 10,
|
|
6
|
+
confirmBeforeAct: true,
|
|
7
|
+
taskConcurrency: 1,
|
|
8
|
+
compactThreshold: 50,
|
|
9
|
+
logLevel: "info",
|
|
10
|
+
rateLimits: void 0
|
|
11
|
+
};
|
|
12
|
+
var DEFAULT_HEARTBEAT_CONFIG = {
|
|
13
|
+
enabled: false,
|
|
14
|
+
intervalMinutes: 30
|
|
15
|
+
};
|
|
16
|
+
function normalizePawConfig(entry) {
|
|
17
|
+
if (typeof entry === "string") {
|
|
18
|
+
return { name: entry };
|
|
19
|
+
}
|
|
20
|
+
return entry;
|
|
21
|
+
}
|
|
22
|
+
function defineConfig(config) {
|
|
23
|
+
return {
|
|
24
|
+
brain: config.brain,
|
|
25
|
+
paws: config.paws ?? [],
|
|
26
|
+
skills: config.skills ?? [],
|
|
27
|
+
loop: {
|
|
28
|
+
...DEFAULT_LOOP_CONFIG,
|
|
29
|
+
...config.loop
|
|
30
|
+
},
|
|
31
|
+
heartbeat: {
|
|
32
|
+
...DEFAULT_HEARTBEAT_CONFIG,
|
|
33
|
+
...config.heartbeat
|
|
34
|
+
},
|
|
35
|
+
toolProfiles: config.toolProfiles
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function loadConfig(configPath) {
|
|
39
|
+
const userConfig = await loadUserConfig(configPath);
|
|
40
|
+
const lockPath = path.join(path.dirname(configPath), ".openvole", "vole.lock.json");
|
|
41
|
+
const lock = await loadLockFile(lockPath);
|
|
42
|
+
return mergeConfigWithLock(userConfig, lock);
|
|
43
|
+
}
|
|
44
|
+
async function loadUserConfig(configPath) {
|
|
45
|
+
const dir = path.dirname(configPath);
|
|
46
|
+
const jsonPath = path.join(dir, "vole.config.json");
|
|
47
|
+
try {
|
|
48
|
+
const raw = await fs.readFile(jsonPath, "utf-8");
|
|
49
|
+
const config = JSON.parse(raw);
|
|
50
|
+
return {
|
|
51
|
+
brain: config.brain,
|
|
52
|
+
paws: config.paws ?? [],
|
|
53
|
+
skills: config.skills ?? [],
|
|
54
|
+
loop: {
|
|
55
|
+
...DEFAULT_LOOP_CONFIG,
|
|
56
|
+
...config.loop
|
|
57
|
+
},
|
|
58
|
+
heartbeat: {
|
|
59
|
+
...DEFAULT_HEARTBEAT_CONFIG,
|
|
60
|
+
...config.heartbeat
|
|
61
|
+
},
|
|
62
|
+
toolProfiles: config.toolProfiles
|
|
63
|
+
};
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
const candidates = [configPath];
|
|
67
|
+
if (configPath.endsWith(".ts")) {
|
|
68
|
+
candidates.push(configPath.replace(/\.ts$/, ".mjs"));
|
|
69
|
+
candidates.push(configPath.replace(/\.ts$/, ".js"));
|
|
70
|
+
}
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
try {
|
|
73
|
+
const module = await import(candidate);
|
|
74
|
+
const config = module.default ?? module;
|
|
75
|
+
return {
|
|
76
|
+
brain: config.brain,
|
|
77
|
+
paws: config.paws ?? [],
|
|
78
|
+
skills: config.skills ?? [],
|
|
79
|
+
loop: {
|
|
80
|
+
...DEFAULT_LOOP_CONFIG,
|
|
81
|
+
...config.loop
|
|
82
|
+
},
|
|
83
|
+
heartbeat: {
|
|
84
|
+
...DEFAULT_HEARTBEAT_CONFIG,
|
|
85
|
+
...config.heartbeat
|
|
86
|
+
},
|
|
87
|
+
toolProfiles: config.toolProfiles
|
|
88
|
+
};
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
console.warn(`[config] No config found (tried: ${jsonPath}, ${candidates.join(", ")}), using defaults`);
|
|
94
|
+
return defineConfig({});
|
|
95
|
+
}
|
|
96
|
+
async function loadLockFile(lockPath) {
|
|
97
|
+
try {
|
|
98
|
+
const raw = await fs.readFile(lockPath, "utf-8");
|
|
99
|
+
return JSON.parse(raw);
|
|
100
|
+
} catch {
|
|
101
|
+
return { paws: [], skills: [] };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function mergeConfigWithLock(userConfig, lock) {
|
|
105
|
+
const userPawNames = new Set(
|
|
106
|
+
userConfig.paws.map((p) => typeof p === "string" ? p : p.name)
|
|
107
|
+
);
|
|
108
|
+
const userSkillNames = new Set(userConfig.skills);
|
|
109
|
+
const mergedPaws = [...userConfig.paws];
|
|
110
|
+
for (const lockPaw of lock.paws) {
|
|
111
|
+
if (!userPawNames.has(lockPaw.name)) {
|
|
112
|
+
mergedPaws.push(
|
|
113
|
+
lockPaw.allow ? { name: lockPaw.name, allow: lockPaw.allow } : lockPaw.name
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const mergedSkills = [...userConfig.skills];
|
|
118
|
+
for (const lockSkill of lock.skills) {
|
|
119
|
+
if (!userSkillNames.has(lockSkill.name)) {
|
|
120
|
+
mergedSkills.push(lockSkill.name);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
...userConfig,
|
|
125
|
+
paws: mergedPaws,
|
|
126
|
+
skills: mergedSkills
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function readLockFile(projectRoot) {
|
|
130
|
+
const lockPath = path.join(projectRoot, ".openvole", "vole.lock.json");
|
|
131
|
+
try {
|
|
132
|
+
const raw = await fs.readFile(lockPath, "utf-8");
|
|
133
|
+
return JSON.parse(raw);
|
|
134
|
+
} catch {
|
|
135
|
+
return { paws: [], skills: [] };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function writeLockFile(projectRoot, lock) {
|
|
139
|
+
const openvoleDir = path.join(projectRoot, ".openvole");
|
|
140
|
+
await fs.mkdir(openvoleDir, { recursive: true });
|
|
141
|
+
const lockPath = path.join(openvoleDir, "vole.lock.json");
|
|
142
|
+
await fs.writeFile(lockPath, JSON.stringify(lock, null, 2) + "\n", "utf-8");
|
|
143
|
+
}
|
|
144
|
+
async function addPawToLock(projectRoot, name, version, allow) {
|
|
145
|
+
const lock = await readLockFile(projectRoot);
|
|
146
|
+
const existing = lock.paws.findIndex((p) => p.name === name);
|
|
147
|
+
const entry = { name, version, allow };
|
|
148
|
+
if (existing >= 0) {
|
|
149
|
+
lock.paws[existing] = entry;
|
|
150
|
+
} else {
|
|
151
|
+
lock.paws.push(entry);
|
|
152
|
+
}
|
|
153
|
+
await writeLockFile(projectRoot, lock);
|
|
154
|
+
}
|
|
155
|
+
async function removePawFromLock(projectRoot, name) {
|
|
156
|
+
const lock = await readLockFile(projectRoot);
|
|
157
|
+
lock.paws = lock.paws.filter((p) => p.name !== name);
|
|
158
|
+
await writeLockFile(projectRoot, lock);
|
|
159
|
+
}
|
|
160
|
+
async function addSkillToLock(projectRoot, name, version) {
|
|
161
|
+
const lock = await readLockFile(projectRoot);
|
|
162
|
+
const existing = lock.skills.findIndex((s) => s.name === name);
|
|
163
|
+
const entry = { name, version };
|
|
164
|
+
if (existing >= 0) {
|
|
165
|
+
lock.skills[existing] = entry;
|
|
166
|
+
} else {
|
|
167
|
+
lock.skills.push(entry);
|
|
168
|
+
}
|
|
169
|
+
await writeLockFile(projectRoot, lock);
|
|
170
|
+
}
|
|
171
|
+
async function removeSkillFromLock(projectRoot, name) {
|
|
172
|
+
const lock = await readLockFile(projectRoot);
|
|
173
|
+
lock.skills = lock.skills.filter((s) => s.name !== name);
|
|
174
|
+
await writeLockFile(projectRoot, lock);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/core/bus.ts
|
|
178
|
+
import mitt from "mitt";
|
|
179
|
+
function createMessageBus() {
|
|
180
|
+
return mitt();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/context/types.ts
|
|
184
|
+
function createAgentContext(taskId, maxIterations) {
|
|
185
|
+
return {
|
|
186
|
+
taskId,
|
|
187
|
+
messages: [],
|
|
188
|
+
availableTools: [],
|
|
189
|
+
activeSkills: [],
|
|
190
|
+
metadata: {},
|
|
191
|
+
iteration: 0,
|
|
192
|
+
maxIterations
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/core/errors.ts
|
|
197
|
+
function createActionError(code, message, opts) {
|
|
198
|
+
return {
|
|
199
|
+
code,
|
|
200
|
+
message,
|
|
201
|
+
toolName: opts?.toolName,
|
|
202
|
+
pawName: opts?.pawName,
|
|
203
|
+
details: opts?.details
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function successResult(toolName, pawName, output, durationMs) {
|
|
207
|
+
return { toolName, pawName, success: true, output, durationMs };
|
|
208
|
+
}
|
|
209
|
+
function failureResult(toolName, pawName, error, durationMs) {
|
|
210
|
+
return { toolName, pawName, success: false, error, durationMs };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/skill/resolver.ts
|
|
214
|
+
import { execFileSync } from "child_process";
|
|
215
|
+
|
|
216
|
+
// src/core/logger.ts
|
|
217
|
+
var LEVELS = { error: 0, warn: 1, info: 2, debug: 3, trace: 4 };
|
|
218
|
+
function currentLevel() {
|
|
219
|
+
const env = (process.env.VOLE_LOG_LEVEL ?? "info").toLowerCase();
|
|
220
|
+
return LEVELS[env] ?? LEVELS.info;
|
|
221
|
+
}
|
|
222
|
+
function createLogger(tag) {
|
|
223
|
+
const prefix = `[${tag}]`;
|
|
224
|
+
return {
|
|
225
|
+
error: (msg, ...args) => currentLevel() >= LEVELS.error && console.error(prefix, msg, ...args),
|
|
226
|
+
warn: (msg, ...args) => currentLevel() >= LEVELS.warn && console.warn(prefix, msg, ...args),
|
|
227
|
+
info: (msg, ...args) => currentLevel() >= LEVELS.info && console.info(prefix, msg, ...args),
|
|
228
|
+
debug: (msg, ...args) => currentLevel() >= LEVELS.debug && console.debug(prefix, msg, ...args),
|
|
229
|
+
trace: (msg, ...args) => currentLevel() >= LEVELS.trace && console.debug(prefix, msg, ...args)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/skill/resolver.ts
|
|
234
|
+
var logger = createLogger("skill-resolver");
|
|
235
|
+
function resolveSkills(skills, toolRegistry) {
|
|
236
|
+
for (const skill of skills) {
|
|
237
|
+
const missing = [];
|
|
238
|
+
for (const toolName of skill.definition.requiredTools) {
|
|
239
|
+
if (!toolRegistry.has(toolName)) {
|
|
240
|
+
missing.push(`tool:${toolName}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const requires = skill.definition.requires;
|
|
244
|
+
if (requires) {
|
|
245
|
+
for (const envVar of requires.env) {
|
|
246
|
+
if (!process.env[envVar]) {
|
|
247
|
+
missing.push(`env:${envVar}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const bin of requires.bins) {
|
|
251
|
+
if (!isBinaryAvailable(bin)) {
|
|
252
|
+
missing.push(`bin:${bin}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (requires.anyBins.length > 0) {
|
|
256
|
+
const hasAny = requires.anyBins.some(isBinaryAvailable);
|
|
257
|
+
if (!hasAny) {
|
|
258
|
+
missing.push(`anyBin:${requires.anyBins.join("|")}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const wasActive = skill.active;
|
|
263
|
+
skill.active = missing.length === 0;
|
|
264
|
+
skill.missingTools = missing;
|
|
265
|
+
if (skill.active && !wasActive) {
|
|
266
|
+
const providers = skill.definition.requiredTools.map((t) => toolRegistry.get(t)?.pawName).filter(Boolean);
|
|
267
|
+
const providerInfo = providers.length > 0 ? ` (tools provided by: ${[...new Set(providers)].join(", ")})` : "";
|
|
268
|
+
logger.info(`Skill "${skill.name}" activated${providerInfo}`);
|
|
269
|
+
} else if (!skill.active && wasActive) {
|
|
270
|
+
logger.warn(
|
|
271
|
+
`Skill "${skill.name}" deactivated (missing: ${missing.join(", ")})`
|
|
272
|
+
);
|
|
273
|
+
} else if (!skill.active) {
|
|
274
|
+
logger.debug(
|
|
275
|
+
`Skill "${skill.name}" inactive (missing: ${missing.join(", ")})`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function buildActiveSkills(skills, toolRegistry) {
|
|
281
|
+
return skills.filter((s) => s.active).map((s) => {
|
|
282
|
+
const satisfiedBy = s.definition.requiredTools.map((t) => toolRegistry.get(t)?.pawName).filter((name) => name != null);
|
|
283
|
+
return {
|
|
284
|
+
name: s.name,
|
|
285
|
+
description: s.definition.description,
|
|
286
|
+
satisfiedBy: [...new Set(satisfiedBy)]
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function isBinaryAvailable(name) {
|
|
291
|
+
try {
|
|
292
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
293
|
+
execFileSync(cmd, [name], { stdio: "ignore" });
|
|
294
|
+
return true;
|
|
295
|
+
} catch {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/core/hooks.ts
|
|
301
|
+
var PHASE_ORDER = [
|
|
302
|
+
"perceive",
|
|
303
|
+
"think",
|
|
304
|
+
"act",
|
|
305
|
+
"observe"
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
// src/core/loop.ts
|
|
309
|
+
var logger2 = createLogger("loop");
|
|
310
|
+
var MAX_BRAIN_FAILURES = 3;
|
|
311
|
+
async function runAgentLoop(task, deps) {
|
|
312
|
+
const { bus, toolRegistry, pawRegistry, skillRegistry, io, config, toolProfiles, rateLimiter } = deps;
|
|
313
|
+
const rateLimits = config.rateLimits;
|
|
314
|
+
let toolExecutionCount = 0;
|
|
315
|
+
logger2.info(`Agent loop started for task ${task.id}: "${task.input}"`);
|
|
316
|
+
let context = createAgentContext(task.id, config.maxIterations);
|
|
317
|
+
context.metadata.taskSource = task.source;
|
|
318
|
+
context.metadata.sessionId = task.sessionId;
|
|
319
|
+
if (task.metadata) {
|
|
320
|
+
Object.assign(context.metadata, task.metadata);
|
|
321
|
+
}
|
|
322
|
+
if (task.source === "heartbeat") {
|
|
323
|
+
context.metadata.heartbeat = true;
|
|
324
|
+
}
|
|
325
|
+
context.messages.push({
|
|
326
|
+
role: "user",
|
|
327
|
+
content: task.input,
|
|
328
|
+
timestamp: Date.now()
|
|
329
|
+
});
|
|
330
|
+
logger2.debug("Phase: bootstrap");
|
|
331
|
+
context = await pawRegistry.runBootstrapHooks(context);
|
|
332
|
+
let consecutiveBrainFailures = 0;
|
|
333
|
+
for (context.iteration = 0; context.iteration < config.maxIterations; context.iteration++) {
|
|
334
|
+
if (task.status === "cancelled") {
|
|
335
|
+
logger2.info(`Task ${task.id} cancelled at iteration ${context.iteration}`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (config.compactThreshold > 0 && context.messages.length > config.compactThreshold) {
|
|
339
|
+
logger2.info(
|
|
340
|
+
`Context has ${context.messages.length} messages (threshold: ${config.compactThreshold}), running compact`
|
|
341
|
+
);
|
|
342
|
+
context = await pawRegistry.runCompactHooks(context);
|
|
343
|
+
}
|
|
344
|
+
logger2.info(
|
|
345
|
+
`Loop running \u2014 iteration ${context.iteration + 1}/${config.maxIterations}`
|
|
346
|
+
);
|
|
347
|
+
logger2.debug(`Phase: ${PHASE_ORDER[0]}`);
|
|
348
|
+
const enrichedContext = await runPerceive(context, pawRegistry, toolRegistry, skillRegistry);
|
|
349
|
+
if (rateLimiter && rateLimits) {
|
|
350
|
+
if (rateLimits.llmCallsPerMinute != null) {
|
|
351
|
+
if (!rateLimiter.tryConsume("llm:per-minute", rateLimits.llmCallsPerMinute, 6e4)) {
|
|
352
|
+
logger2.warn(`Rate limit hit: llmCallsPerMinute (${rateLimits.llmCallsPerMinute})`);
|
|
353
|
+
bus.emit("rate:limited", { bucket: "llm:per-minute", source: task.source });
|
|
354
|
+
enrichedContext.messages.push({
|
|
355
|
+
role: "error",
|
|
356
|
+
content: `Rate limit exceeded: LLM calls per minute (limit: ${rateLimits.llmCallsPerMinute}). Retrying next iteration.`,
|
|
357
|
+
timestamp: Date.now()
|
|
358
|
+
});
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (rateLimits.llmCallsPerHour != null) {
|
|
363
|
+
if (!rateLimiter.tryConsume("llm:per-hour", rateLimits.llmCallsPerHour, 36e5)) {
|
|
364
|
+
logger2.warn(`Rate limit hit: llmCallsPerHour (${rateLimits.llmCallsPerHour})`);
|
|
365
|
+
bus.emit("rate:limited", { bucket: "llm:per-hour", source: task.source });
|
|
366
|
+
enrichedContext.messages.push({
|
|
367
|
+
role: "error",
|
|
368
|
+
content: `Rate limit exceeded: LLM calls per hour (limit: ${rateLimits.llmCallsPerHour}). Retrying next iteration.`,
|
|
369
|
+
timestamp: Date.now()
|
|
370
|
+
});
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
logger2.debug(`Phase: ${PHASE_ORDER[1]}`);
|
|
376
|
+
const plan = await runThink(enrichedContext, pawRegistry);
|
|
377
|
+
if (!plan) {
|
|
378
|
+
logger2.debug("Think phase returned null (no Brain Paw)");
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
if (plan === "BRAIN_ERROR") {
|
|
382
|
+
consecutiveBrainFailures++;
|
|
383
|
+
if (consecutiveBrainFailures >= MAX_BRAIN_FAILURES) {
|
|
384
|
+
io.notify(
|
|
385
|
+
`Brain Paw failed ${MAX_BRAIN_FAILURES} consecutive times. Halting task ${task.id}.`
|
|
386
|
+
);
|
|
387
|
+
task.error = `Brain failed ${MAX_BRAIN_FAILURES} consecutive times`;
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
consecutiveBrainFailures = 0;
|
|
393
|
+
if (plan.done) {
|
|
394
|
+
if (plan.response) {
|
|
395
|
+
task.result = plan.response;
|
|
396
|
+
io.notify(plan.response);
|
|
397
|
+
enrichedContext.messages.push({
|
|
398
|
+
role: "brain",
|
|
399
|
+
content: plan.response,
|
|
400
|
+
timestamp: Date.now()
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
logger2.info(`Task ${task.id} completed by Brain at iteration ${context.iteration + 1}`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (plan.response) {
|
|
407
|
+
io.notify(plan.response);
|
|
408
|
+
enrichedContext.messages.push({
|
|
409
|
+
role: "brain",
|
|
410
|
+
content: plan.response,
|
|
411
|
+
timestamp: Date.now()
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
logger2.debug(`Phase: ${PHASE_ORDER[2]}`);
|
|
415
|
+
if (plan.actions.length > 0) {
|
|
416
|
+
if (rateLimiter && rateLimits?.toolExecutionsPerTask != null) {
|
|
417
|
+
const incoming = plan.actions.length;
|
|
418
|
+
if (toolExecutionCount + incoming > rateLimits.toolExecutionsPerTask) {
|
|
419
|
+
logger2.warn(
|
|
420
|
+
`Rate limit hit: toolExecutionsPerTask (${toolExecutionCount + incoming}/${rateLimits.toolExecutionsPerTask})`
|
|
421
|
+
);
|
|
422
|
+
bus.emit("rate:limited", { bucket: "tools:per-task", source: task.source });
|
|
423
|
+
enrichedContext.messages.push({
|
|
424
|
+
role: "error",
|
|
425
|
+
content: `Rate limit exceeded: tool executions per task (limit: ${rateLimits.toolExecutionsPerTask}, used: ${toolExecutionCount}). Stopping task.`,
|
|
426
|
+
timestamp: Date.now()
|
|
427
|
+
});
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const profile = toolProfiles?.[task.source];
|
|
432
|
+
if (profile) {
|
|
433
|
+
const blocked = [];
|
|
434
|
+
plan.actions = plan.actions.filter((a) => {
|
|
435
|
+
if (profile.allow && !profile.allow.includes(a.tool)) {
|
|
436
|
+
blocked.push(a.tool);
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
if (profile.deny?.includes(a.tool)) {
|
|
440
|
+
blocked.push(a.tool);
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
return true;
|
|
444
|
+
});
|
|
445
|
+
if (blocked.length > 0) {
|
|
446
|
+
logger2.warn(`Blocked tools for source "${task.source}": ${blocked.join(", ")}`);
|
|
447
|
+
enrichedContext.messages.push({
|
|
448
|
+
role: "error",
|
|
449
|
+
content: `Tools blocked by security profile for "${task.source}" source: ${blocked.join(", ")}`,
|
|
450
|
+
timestamp: Date.now()
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
if (plan.actions.length === 0) continue;
|
|
454
|
+
}
|
|
455
|
+
for (const action of plan.actions) {
|
|
456
|
+
logger2.info(`Tool call: ${action.tool}(${JSON.stringify(action.params)})`);
|
|
457
|
+
}
|
|
458
|
+
if (config.confirmBeforeAct) {
|
|
459
|
+
const toolNames = plan.actions.map((a) => a.tool).join(", ");
|
|
460
|
+
const confirmed = await io.confirm(
|
|
461
|
+
`Execute tools: ${toolNames}?`
|
|
462
|
+
);
|
|
463
|
+
if (!confirmed) {
|
|
464
|
+
enrichedContext.messages.push({
|
|
465
|
+
role: "error",
|
|
466
|
+
content: "User declined to execute planned actions",
|
|
467
|
+
timestamp: Date.now()
|
|
468
|
+
});
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const results = await runAct(
|
|
473
|
+
plan.actions,
|
|
474
|
+
plan.execution ?? "sequential",
|
|
475
|
+
enrichedContext,
|
|
476
|
+
toolRegistry,
|
|
477
|
+
pawRegistry
|
|
478
|
+
);
|
|
479
|
+
toolExecutionCount += results.length;
|
|
480
|
+
logger2.debug(`Phase: ${PHASE_ORDER[3]}`);
|
|
481
|
+
for (const result of results) {
|
|
482
|
+
if (result.success) {
|
|
483
|
+
enrichedContext.messages.push({
|
|
484
|
+
role: "tool_result",
|
|
485
|
+
content: typeof result.output === "string" ? result.output : JSON.stringify(result.output),
|
|
486
|
+
toolCall: {
|
|
487
|
+
name: result.toolName,
|
|
488
|
+
params: null
|
|
489
|
+
},
|
|
490
|
+
timestamp: Date.now()
|
|
491
|
+
});
|
|
492
|
+
} else {
|
|
493
|
+
enrichedContext.messages.push({
|
|
494
|
+
role: "error",
|
|
495
|
+
content: result.error?.message ?? "Unknown tool error",
|
|
496
|
+
toolCall: {
|
|
497
|
+
name: result.toolName,
|
|
498
|
+
params: null
|
|
499
|
+
},
|
|
500
|
+
timestamp: Date.now()
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
pawRegistry.runObserveHooks(result);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
Object.assign(context, enrichedContext);
|
|
507
|
+
}
|
|
508
|
+
if (context.iteration >= config.maxIterations) {
|
|
509
|
+
logger2.warn(
|
|
510
|
+
`Task ${task.id} reached max iterations (${config.maxIterations})`
|
|
511
|
+
);
|
|
512
|
+
io.notify(
|
|
513
|
+
`Task reached maximum iterations (${config.maxIterations}). Stopping.`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async function runPerceive(context, pawRegistry, toolRegistry, skillRegistry) {
|
|
518
|
+
const enriched = { ...context };
|
|
519
|
+
enriched.availableTools = toolRegistry.summaries();
|
|
520
|
+
enriched.activeSkills = buildActiveSkills(
|
|
521
|
+
skillRegistry.list(),
|
|
522
|
+
toolRegistry
|
|
523
|
+
);
|
|
524
|
+
return pawRegistry.runGlobalPerceiveHooks(enriched);
|
|
525
|
+
}
|
|
526
|
+
async function runThink(context, pawRegistry) {
|
|
527
|
+
try {
|
|
528
|
+
return await pawRegistry.think(context);
|
|
529
|
+
} catch (err) {
|
|
530
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
531
|
+
logger2.error(`Brain error: ${message}`);
|
|
532
|
+
context.messages.push({
|
|
533
|
+
role: "error",
|
|
534
|
+
content: `Brain error: ${message}`,
|
|
535
|
+
timestamp: Date.now()
|
|
536
|
+
});
|
|
537
|
+
return "BRAIN_ERROR";
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async function runAct(actions, execution, context, toolRegistry, pawRegistry) {
|
|
541
|
+
const pawNames = /* @__PURE__ */ new Set();
|
|
542
|
+
for (const action of actions) {
|
|
543
|
+
const tool = toolRegistry.get(action.tool);
|
|
544
|
+
if (tool) pawNames.add(tool.pawName);
|
|
545
|
+
}
|
|
546
|
+
for (const pawName of pawNames) {
|
|
547
|
+
await pawRegistry.runLazyPerceive(pawName, context);
|
|
548
|
+
}
|
|
549
|
+
if (execution === "parallel") {
|
|
550
|
+
return Promise.all(
|
|
551
|
+
actions.map(
|
|
552
|
+
(action) => executeSingleAction(action, toolRegistry, pawRegistry)
|
|
553
|
+
)
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
const results = [];
|
|
557
|
+
for (const action of actions) {
|
|
558
|
+
const result = await executeSingleAction(action, toolRegistry, pawRegistry);
|
|
559
|
+
results.push(result);
|
|
560
|
+
}
|
|
561
|
+
return results;
|
|
562
|
+
}
|
|
563
|
+
async function executeSingleAction(action, toolRegistry, pawRegistry) {
|
|
564
|
+
const startTime = Date.now();
|
|
565
|
+
const tool = toolRegistry.get(action.tool);
|
|
566
|
+
if (!tool) {
|
|
567
|
+
return failureResult(
|
|
568
|
+
action.tool,
|
|
569
|
+
"unknown",
|
|
570
|
+
createActionError("TOOL_NOT_FOUND", `Tool "${action.tool}" not found`, {
|
|
571
|
+
toolName: action.tool
|
|
572
|
+
}),
|
|
573
|
+
Date.now() - startTime
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
if (!tool.inProcess && !pawRegistry.isHealthy(tool.pawName)) {
|
|
577
|
+
return failureResult(
|
|
578
|
+
action.tool,
|
|
579
|
+
tool.pawName,
|
|
580
|
+
createActionError("PAW_CRASHED", `Paw "${tool.pawName}" is not healthy`, {
|
|
581
|
+
toolName: action.tool,
|
|
582
|
+
pawName: tool.pawName
|
|
583
|
+
}),
|
|
584
|
+
Date.now() - startTime
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
if (tool.parameters && typeof tool.parameters.parse === "function") {
|
|
589
|
+
tool.parameters.parse(action.params);
|
|
590
|
+
}
|
|
591
|
+
const output = await tool.execute(action.params);
|
|
592
|
+
return successResult(action.tool, tool.pawName, output, Date.now() - startTime);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
595
|
+
const isTimeout = message.toLowerCase().includes("timeout");
|
|
596
|
+
const isPermission = message.toLowerCase().includes("permission");
|
|
597
|
+
const code = isTimeout ? "TOOL_TIMEOUT" : isPermission ? "PERMISSION_DENIED" : "TOOL_EXCEPTION";
|
|
598
|
+
return failureResult(
|
|
599
|
+
action.tool,
|
|
600
|
+
tool.pawName,
|
|
601
|
+
createActionError(code, message, {
|
|
602
|
+
toolName: action.tool,
|
|
603
|
+
pawName: tool.pawName,
|
|
604
|
+
details: err instanceof Error ? err.stack : void 0
|
|
605
|
+
}),
|
|
606
|
+
Date.now() - startTime
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/core/task.ts
|
|
612
|
+
import * as crypto from "crypto";
|
|
613
|
+
var logger3 = createLogger("task-queue");
|
|
614
|
+
var TaskQueue = class {
|
|
615
|
+
constructor(bus, concurrency = 1, rateLimiter, rateLimits) {
|
|
616
|
+
this.bus = bus;
|
|
617
|
+
this.concurrency = concurrency;
|
|
618
|
+
this.rateLimiter = rateLimiter;
|
|
619
|
+
this.rateLimits = rateLimits;
|
|
620
|
+
}
|
|
621
|
+
queue = [];
|
|
622
|
+
running = /* @__PURE__ */ new Map();
|
|
623
|
+
completed = [];
|
|
624
|
+
runner;
|
|
625
|
+
draining = false;
|
|
626
|
+
/** Set the task runner function (called by the agent loop) */
|
|
627
|
+
setRunner(runner) {
|
|
628
|
+
this.runner = runner;
|
|
629
|
+
}
|
|
630
|
+
/** Enqueue a new task */
|
|
631
|
+
enqueue(input, source = "user", options) {
|
|
632
|
+
const task = {
|
|
633
|
+
id: crypto.randomUUID(),
|
|
634
|
+
source,
|
|
635
|
+
input,
|
|
636
|
+
status: "queued",
|
|
637
|
+
createdAt: Date.now(),
|
|
638
|
+
sessionId: options?.sessionId,
|
|
639
|
+
metadata: options?.metadata
|
|
640
|
+
};
|
|
641
|
+
if (this.rateLimiter && this.rateLimits?.tasksPerHour) {
|
|
642
|
+
const limit = this.rateLimits.tasksPerHour[source];
|
|
643
|
+
if (limit != null) {
|
|
644
|
+
const bucket = `tasks:per-hour:${source}`;
|
|
645
|
+
if (!this.rateLimiter.tryConsume(bucket, limit, 36e5)) {
|
|
646
|
+
logger3.warn(
|
|
647
|
+
`Rate limit warning: tasksPerHour for source "${source}" exceeded (limit: ${limit}). Task will still be enqueued.`
|
|
648
|
+
);
|
|
649
|
+
this.bus.emit("rate:limited", { bucket, source });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
this.queue.push(task);
|
|
654
|
+
logger3.info(`Task ${task.id} queued (source: ${source})`);
|
|
655
|
+
this.bus.emit("task:queued", { taskId: task.id });
|
|
656
|
+
this.drain();
|
|
657
|
+
return task;
|
|
658
|
+
}
|
|
659
|
+
/** Cancel a task by ID */
|
|
660
|
+
cancel(taskId) {
|
|
661
|
+
const queueIdx = this.queue.findIndex((t) => t.id === taskId);
|
|
662
|
+
if (queueIdx !== -1) {
|
|
663
|
+
const task = this.queue.splice(queueIdx, 1)[0];
|
|
664
|
+
task.status = "cancelled";
|
|
665
|
+
task.completedAt = Date.now();
|
|
666
|
+
this.completed.push(task);
|
|
667
|
+
logger3.info(`Task ${taskId} cancelled (was queued)`);
|
|
668
|
+
this.bus.emit("task:cancelled", { taskId });
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
const running = this.running.get(taskId);
|
|
672
|
+
if (running) {
|
|
673
|
+
running.status = "cancelled";
|
|
674
|
+
logger3.info(`Task ${taskId} marked for cancellation (running)`);
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
/** Get all tasks (queued + running + completed) */
|
|
680
|
+
list() {
|
|
681
|
+
return [
|
|
682
|
+
...this.queue,
|
|
683
|
+
...Array.from(this.running.values()),
|
|
684
|
+
...this.completed.slice(-50)
|
|
685
|
+
// keep last 50 completed
|
|
686
|
+
];
|
|
687
|
+
}
|
|
688
|
+
/** Get a task by ID */
|
|
689
|
+
get(taskId) {
|
|
690
|
+
return this.queue.find((t) => t.id === taskId) ?? this.running.get(taskId) ?? this.completed.find((t) => t.id === taskId);
|
|
691
|
+
}
|
|
692
|
+
/** Check if a task has been cancelled */
|
|
693
|
+
isCancelled(taskId) {
|
|
694
|
+
const task = this.running.get(taskId);
|
|
695
|
+
return task?.status === "cancelled";
|
|
696
|
+
}
|
|
697
|
+
async drain() {
|
|
698
|
+
if (this.draining) return;
|
|
699
|
+
this.draining = true;
|
|
700
|
+
try {
|
|
701
|
+
while (this.queue.length > 0 && this.running.size < this.concurrency) {
|
|
702
|
+
if (!this.runner) {
|
|
703
|
+
logger3.error("No task runner configured");
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
const task = this.queue.shift();
|
|
707
|
+
task.status = "running";
|
|
708
|
+
task.startedAt = Date.now();
|
|
709
|
+
this.running.set(task.id, task);
|
|
710
|
+
logger3.info(`Task ${task.id} started`);
|
|
711
|
+
this.bus.emit("task:started", { taskId: task.id });
|
|
712
|
+
this.runTask(task);
|
|
713
|
+
}
|
|
714
|
+
} finally {
|
|
715
|
+
this.draining = false;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async runTask(task) {
|
|
719
|
+
try {
|
|
720
|
+
await this.runner(task);
|
|
721
|
+
if (task.status !== "cancelled") {
|
|
722
|
+
task.status = "completed";
|
|
723
|
+
}
|
|
724
|
+
task.completedAt = Date.now();
|
|
725
|
+
logger3.info(`Task ${task.id} ${task.status}`);
|
|
726
|
+
this.bus.emit("task:completed", { taskId: task.id, result: task.result });
|
|
727
|
+
} catch (err) {
|
|
728
|
+
task.status = "failed";
|
|
729
|
+
task.completedAt = Date.now();
|
|
730
|
+
task.error = err instanceof Error ? err.message : String(err);
|
|
731
|
+
logger3.error(`Task ${task.id} failed: ${task.error}`);
|
|
732
|
+
this.bus.emit("task:failed", { taskId: task.id, error: err });
|
|
733
|
+
} finally {
|
|
734
|
+
this.running.delete(task.id);
|
|
735
|
+
this.completed.push(task);
|
|
736
|
+
this.drain();
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
// src/core/scheduler.ts
|
|
742
|
+
var logger4 = createLogger("scheduler");
|
|
743
|
+
var SchedulerStore = class {
|
|
744
|
+
schedules = /* @__PURE__ */ new Map();
|
|
745
|
+
/** Create or replace a recurring schedule */
|
|
746
|
+
add(id, input, intervalMinutes, onTick) {
|
|
747
|
+
if (this.schedules.has(id)) {
|
|
748
|
+
this.cancel(id);
|
|
749
|
+
}
|
|
750
|
+
const intervalMs = intervalMinutes * 6e4;
|
|
751
|
+
const timer = setInterval(onTick, intervalMs);
|
|
752
|
+
this.schedules.set(id, {
|
|
753
|
+
id,
|
|
754
|
+
input,
|
|
755
|
+
intervalMinutes,
|
|
756
|
+
timer,
|
|
757
|
+
createdAt: Date.now()
|
|
758
|
+
});
|
|
759
|
+
logger4.info(`Schedule "${id}" created \u2014 every ${intervalMinutes}m: "${input.substring(0, 80)}"`);
|
|
760
|
+
}
|
|
761
|
+
/** Cancel a schedule by ID */
|
|
762
|
+
cancel(id) {
|
|
763
|
+
const entry = this.schedules.get(id);
|
|
764
|
+
if (!entry) return false;
|
|
765
|
+
clearInterval(entry.timer);
|
|
766
|
+
this.schedules.delete(id);
|
|
767
|
+
logger4.info(`Schedule "${id}" cancelled`);
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
/** List all active schedules */
|
|
771
|
+
list() {
|
|
772
|
+
return Array.from(this.schedules.values()).map(({ id, input, intervalMinutes, createdAt }) => ({
|
|
773
|
+
id,
|
|
774
|
+
input,
|
|
775
|
+
intervalMinutes,
|
|
776
|
+
createdAt
|
|
777
|
+
}));
|
|
778
|
+
}
|
|
779
|
+
/** Clear all schedules (for shutdown) */
|
|
780
|
+
clearAll() {
|
|
781
|
+
for (const entry of this.schedules.values()) {
|
|
782
|
+
clearInterval(entry.timer);
|
|
783
|
+
}
|
|
784
|
+
this.schedules.clear();
|
|
785
|
+
logger4.info("All schedules cleared");
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
// src/core/rate-limiter.ts
|
|
790
|
+
var logger5 = createLogger("rate-limiter");
|
|
791
|
+
var RateLimiter = class {
|
|
792
|
+
buckets = /* @__PURE__ */ new Map();
|
|
793
|
+
/**
|
|
794
|
+
* Try to consume one token from the bucket.
|
|
795
|
+
* Returns true if the request is under the limit, false if rate-limited.
|
|
796
|
+
*/
|
|
797
|
+
tryConsume(bucket, limit, windowMs) {
|
|
798
|
+
const now = Date.now();
|
|
799
|
+
this.cleanup(bucket, now, windowMs);
|
|
800
|
+
const timestamps = this.buckets.get(bucket) ?? [];
|
|
801
|
+
if (timestamps.length >= limit) {
|
|
802
|
+
logger5.debug(`Bucket "${bucket}" rate-limited: ${timestamps.length}/${limit} in ${windowMs}ms window`);
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
timestamps.push(now);
|
|
806
|
+
this.buckets.set(bucket, timestamps);
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Returns the number of remaining tokens in the bucket for the current window.
|
|
811
|
+
*/
|
|
812
|
+
remaining(bucket, limit, windowMs) {
|
|
813
|
+
const now = Date.now();
|
|
814
|
+
this.cleanup(bucket, now, windowMs);
|
|
815
|
+
const timestamps = this.buckets.get(bucket) ?? [];
|
|
816
|
+
return Math.max(0, limit - timestamps.length);
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Remove expired timestamps from a bucket.
|
|
820
|
+
*/
|
|
821
|
+
cleanup(bucket, now, windowMs) {
|
|
822
|
+
const timestamps = this.buckets.get(bucket);
|
|
823
|
+
if (!timestamps) return;
|
|
824
|
+
const cutoff = now - windowMs;
|
|
825
|
+
const filtered = timestamps.filter((t) => t > cutoff);
|
|
826
|
+
if (filtered.length === 0) {
|
|
827
|
+
this.buckets.delete(bucket);
|
|
828
|
+
} else {
|
|
829
|
+
this.buckets.set(bucket, filtered);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// src/tool/registry.ts
|
|
835
|
+
var logger6 = createLogger("tool-registry");
|
|
836
|
+
var ToolRegistry = class {
|
|
837
|
+
constructor(bus) {
|
|
838
|
+
this.bus = bus;
|
|
839
|
+
}
|
|
840
|
+
tools = /* @__PURE__ */ new Map();
|
|
841
|
+
/** Register tools from a Paw. Skips tools with conflicting names. */
|
|
842
|
+
register(pawName, tools, inProcess) {
|
|
843
|
+
for (const tool of tools) {
|
|
844
|
+
if (this.tools.has(tool.name)) {
|
|
845
|
+
const existing = this.tools.get(tool.name);
|
|
846
|
+
logger6.warn(
|
|
847
|
+
`Tool name conflict: "${tool.name}" already registered by "${existing.pawName}", ignoring registration from "${pawName}"`
|
|
848
|
+
);
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
this.tools.set(tool.name, {
|
|
852
|
+
name: tool.name,
|
|
853
|
+
description: tool.description,
|
|
854
|
+
parameters: tool.parameters,
|
|
855
|
+
pawName,
|
|
856
|
+
inProcess,
|
|
857
|
+
execute: tool.execute
|
|
858
|
+
});
|
|
859
|
+
logger6.info(`Registered tool "${tool.name}" from "${pawName}"`);
|
|
860
|
+
this.bus.emit("tool:registered", { toolName: tool.name, pawName });
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
/** Remove all tools owned by a specific Paw */
|
|
864
|
+
unregister(pawName) {
|
|
865
|
+
const toRemove = [];
|
|
866
|
+
for (const [name, entry] of this.tools) {
|
|
867
|
+
if (entry.pawName === pawName) {
|
|
868
|
+
toRemove.push(name);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
for (const name of toRemove) {
|
|
872
|
+
this.tools.delete(name);
|
|
873
|
+
logger6.info(`Unregistered tool "${name}" from "${pawName}"`);
|
|
874
|
+
this.bus.emit("tool:unregistered", { toolName: name, pawName });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
/** Get a tool entry by name */
|
|
878
|
+
get(toolName) {
|
|
879
|
+
return this.tools.get(toolName);
|
|
880
|
+
}
|
|
881
|
+
/** List all registered tools */
|
|
882
|
+
list() {
|
|
883
|
+
return Array.from(this.tools.values());
|
|
884
|
+
}
|
|
885
|
+
/** Check if a tool exists */
|
|
886
|
+
has(toolName) {
|
|
887
|
+
return this.tools.has(toolName);
|
|
888
|
+
}
|
|
889
|
+
/** Get tool summaries for AgentContext */
|
|
890
|
+
summaries() {
|
|
891
|
+
return this.list().map((t) => ({
|
|
892
|
+
name: t.name,
|
|
893
|
+
description: t.description,
|
|
894
|
+
pawName: t.pawName
|
|
895
|
+
}));
|
|
896
|
+
}
|
|
897
|
+
/** Get all tool names owned by a specific Paw */
|
|
898
|
+
toolsForPaw(pawName) {
|
|
899
|
+
return this.list().filter((t) => t.pawName === pawName).map((t) => t.name);
|
|
900
|
+
}
|
|
901
|
+
/** Clear all tools (for shutdown) */
|
|
902
|
+
clear() {
|
|
903
|
+
this.tools.clear();
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
// src/paw/manifest.ts
|
|
908
|
+
import * as fs2 from "fs/promises";
|
|
909
|
+
import { accessSync } from "fs";
|
|
910
|
+
import * as path2 from "path";
|
|
911
|
+
import { z } from "zod";
|
|
912
|
+
var logger7 = createLogger("paw-manifest");
|
|
913
|
+
var pawManifestSchema = z.object({
|
|
914
|
+
name: z.string().min(1),
|
|
915
|
+
version: z.string().min(1),
|
|
916
|
+
description: z.string(),
|
|
917
|
+
entry: z.string().min(1),
|
|
918
|
+
brain: z.boolean().default(false),
|
|
919
|
+
inProcess: z.boolean().optional().default(false),
|
|
920
|
+
transport: z.enum(["ipc", "stdio"]).optional().default("ipc"),
|
|
921
|
+
tools: z.array(
|
|
922
|
+
z.object({
|
|
923
|
+
name: z.string().min(1),
|
|
924
|
+
description: z.string()
|
|
925
|
+
})
|
|
926
|
+
).default([]),
|
|
927
|
+
permissions: z.object({
|
|
928
|
+
network: z.array(z.string()).optional().default([]),
|
|
929
|
+
listen: z.array(z.number().int().positive()).optional().default([]),
|
|
930
|
+
filesystem: z.array(z.string()).optional().default([]),
|
|
931
|
+
env: z.array(z.string()).optional().default([])
|
|
932
|
+
}).optional().default({})
|
|
933
|
+
});
|
|
934
|
+
function resolvePawPath(name, projectRoot) {
|
|
935
|
+
if (name.startsWith(".") || name.startsWith("/")) {
|
|
936
|
+
return path2.resolve(projectRoot, name);
|
|
937
|
+
}
|
|
938
|
+
const openvoleDir = path2.resolve(projectRoot, ".openvole", "paws", name);
|
|
939
|
+
try {
|
|
940
|
+
accessSync(path2.join(openvoleDir, "vole-paw.json"));
|
|
941
|
+
return openvoleDir;
|
|
942
|
+
} catch {
|
|
943
|
+
}
|
|
944
|
+
return path2.resolve(projectRoot, "node_modules", name);
|
|
945
|
+
}
|
|
946
|
+
async function readPawManifest(pawPath) {
|
|
947
|
+
const manifestPath = path2.join(pawPath, "vole-paw.json");
|
|
948
|
+
try {
|
|
949
|
+
const raw = await fs2.readFile(manifestPath, "utf-8");
|
|
950
|
+
const parsed = JSON.parse(raw);
|
|
951
|
+
const result = pawManifestSchema.safeParse(parsed);
|
|
952
|
+
if (!result.success) {
|
|
953
|
+
logger7.error(
|
|
954
|
+
"Invalid manifest at %s: %s",
|
|
955
|
+
manifestPath,
|
|
956
|
+
result.error.message
|
|
957
|
+
);
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
return result.data;
|
|
961
|
+
} catch (err) {
|
|
962
|
+
if (err.code === "ENOENT") {
|
|
963
|
+
logger7.error("Manifest not found: %s", manifestPath);
|
|
964
|
+
} else {
|
|
965
|
+
logger7.error("Failed to read manifest %s: %s", manifestPath, err);
|
|
966
|
+
}
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/paw/loader.ts
|
|
972
|
+
import * as path3 from "path";
|
|
973
|
+
import { execa } from "execa";
|
|
974
|
+
|
|
975
|
+
// src/core/ipc.ts
|
|
976
|
+
import * as crypto2 from "crypto";
|
|
977
|
+
var logger8 = createLogger("ipc");
|
|
978
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
979
|
+
var IpcTransport = class {
|
|
980
|
+
constructor(type, childProcess, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
981
|
+
this.type = type;
|
|
982
|
+
this.childProcess = childProcess;
|
|
983
|
+
this.timeoutMs = timeoutMs;
|
|
984
|
+
if (type === "ipc") {
|
|
985
|
+
this.setupIpcListeners();
|
|
986
|
+
} else {
|
|
987
|
+
this.setupStdioListeners();
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
pending = /* @__PURE__ */ new Map();
|
|
991
|
+
handlers = /* @__PURE__ */ new Map();
|
|
992
|
+
disposed = false;
|
|
993
|
+
/** Register a handler for incoming requests from the Paw */
|
|
994
|
+
onRequest(method, handler) {
|
|
995
|
+
this.handlers.set(method, handler);
|
|
996
|
+
}
|
|
997
|
+
/** Send a request to the Paw and wait for a response */
|
|
998
|
+
async request(method, params) {
|
|
999
|
+
if (this.disposed) {
|
|
1000
|
+
throw new Error("Transport has been disposed");
|
|
1001
|
+
}
|
|
1002
|
+
const id = crypto2.randomUUID();
|
|
1003
|
+
const message = { jsonrpc: "2.0", id, method, params };
|
|
1004
|
+
logger8.trace("Sending request: %s %s", method, JSON.stringify(params ?? "", null, 2));
|
|
1005
|
+
return new Promise((resolve6, reject) => {
|
|
1006
|
+
const timer = setTimeout(() => {
|
|
1007
|
+
this.pending.delete(id);
|
|
1008
|
+
reject(new Error(`IPC request "${method}" timed out after ${this.timeoutMs}ms`));
|
|
1009
|
+
}, this.timeoutMs);
|
|
1010
|
+
this.pending.set(id, { resolve: resolve6, reject, timer });
|
|
1011
|
+
this.send(message);
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
/** Send a notification (no response expected) */
|
|
1015
|
+
notify(method, params) {
|
|
1016
|
+
if (this.disposed) return;
|
|
1017
|
+
const message = { jsonrpc: "2.0", method, params };
|
|
1018
|
+
this.send(message);
|
|
1019
|
+
}
|
|
1020
|
+
/** Clean up resources */
|
|
1021
|
+
dispose() {
|
|
1022
|
+
this.disposed = true;
|
|
1023
|
+
for (const [, pending] of this.pending) {
|
|
1024
|
+
clearTimeout(pending.timer);
|
|
1025
|
+
pending.reject(new Error("Transport disposed"));
|
|
1026
|
+
}
|
|
1027
|
+
this.pending.clear();
|
|
1028
|
+
this.handlers.clear();
|
|
1029
|
+
}
|
|
1030
|
+
send(message) {
|
|
1031
|
+
if (this.type === "ipc") {
|
|
1032
|
+
this.childProcess.send?.(message);
|
|
1033
|
+
} else {
|
|
1034
|
+
const json = JSON.stringify(message);
|
|
1035
|
+
const header = `Content-Length: ${Buffer.byteLength(json)}\r
|
|
1036
|
+
\r
|
|
1037
|
+
`;
|
|
1038
|
+
const stdin = this.childProcess.stdin;
|
|
1039
|
+
if (stdin?.writable) {
|
|
1040
|
+
stdin.write(header + json);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
handleMessage(msg) {
|
|
1045
|
+
logger8.trace("Received message: %s %s", msg.method ?? msg.id ?? "unknown", JSON.stringify(msg.params ?? msg.result ?? "", null, 2));
|
|
1046
|
+
if (msg.id && this.pending.has(msg.id)) {
|
|
1047
|
+
const pending = this.pending.get(msg.id);
|
|
1048
|
+
this.pending.delete(msg.id);
|
|
1049
|
+
clearTimeout(pending.timer);
|
|
1050
|
+
if (msg.error) {
|
|
1051
|
+
pending.reject(new Error(`${msg.error.message} (code: ${msg.error.code})`));
|
|
1052
|
+
} else {
|
|
1053
|
+
pending.resolve(msg.result);
|
|
1054
|
+
}
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
if (msg.method && this.handlers.has(msg.method)) {
|
|
1058
|
+
const handler = this.handlers.get(msg.method);
|
|
1059
|
+
handler(msg.params).then((result) => {
|
|
1060
|
+
if (msg.id) {
|
|
1061
|
+
this.send({ jsonrpc: "2.0", id: msg.id, result });
|
|
1062
|
+
}
|
|
1063
|
+
}).catch((err) => {
|
|
1064
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1065
|
+
logger8.error('Handler error for "%s": %s', msg.method, errorMessage);
|
|
1066
|
+
if (msg.id) {
|
|
1067
|
+
this.send({
|
|
1068
|
+
jsonrpc: "2.0",
|
|
1069
|
+
id: msg.id,
|
|
1070
|
+
error: { code: -32603, message: errorMessage }
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
setupIpcListeners() {
|
|
1077
|
+
this.childProcess.on("message", (msg) => {
|
|
1078
|
+
this.handleMessage(msg);
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
setupStdioListeners() {
|
|
1082
|
+
const stdout = this.childProcess.stdout;
|
|
1083
|
+
if (!stdout) {
|
|
1084
|
+
logger8.error("No stdout stream available for stdio transport");
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
let buffer = "";
|
|
1088
|
+
stdout.setEncoding("utf-8");
|
|
1089
|
+
stdout.on("data", (chunk) => {
|
|
1090
|
+
buffer += chunk;
|
|
1091
|
+
while (buffer.length > 0) {
|
|
1092
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
1093
|
+
if (headerEnd === -1) break;
|
|
1094
|
+
const header = buffer.substring(0, headerEnd);
|
|
1095
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
1096
|
+
if (!match) {
|
|
1097
|
+
logger8.error("Invalid header in stdio transport: %s", header);
|
|
1098
|
+
buffer = buffer.substring(headerEnd + 4);
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
const contentLength = Number.parseInt(match[1], 10);
|
|
1102
|
+
const bodyStart = headerEnd + 4;
|
|
1103
|
+
if (buffer.length < bodyStart + contentLength) break;
|
|
1104
|
+
const body = buffer.substring(bodyStart, bodyStart + contentLength);
|
|
1105
|
+
buffer = buffer.substring(bodyStart + contentLength);
|
|
1106
|
+
try {
|
|
1107
|
+
const msg = JSON.parse(body);
|
|
1108
|
+
this.handleMessage(msg);
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
logger8.error("Failed to parse stdio JSON-RPC message: %s", err);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
function createTransport(type, childProcess, timeoutMs) {
|
|
1117
|
+
return new IpcTransport(type, childProcess, timeoutMs);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/paw/sandbox.ts
|
|
1121
|
+
var logger9 = createLogger("paw-sandbox");
|
|
1122
|
+
function computeEffectivePermissions(manifest, config) {
|
|
1123
|
+
const requested = manifest.permissions ?? { network: [], listen: [], filesystem: [], env: [] };
|
|
1124
|
+
const granted = config.allow;
|
|
1125
|
+
if (!granted) {
|
|
1126
|
+
return {
|
|
1127
|
+
network: requested.network ?? [],
|
|
1128
|
+
listen: requested.listen ?? [],
|
|
1129
|
+
filesystem: requested.filesystem ?? [],
|
|
1130
|
+
env: requested.env ?? []
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
return {
|
|
1134
|
+
network: intersectStrings(requested.network ?? [], granted.network ?? []),
|
|
1135
|
+
listen: intersectNumbers(requested.listen ?? [], granted.listen ?? []),
|
|
1136
|
+
filesystem: intersectStrings(requested.filesystem ?? [], granted.filesystem ?? []),
|
|
1137
|
+
env: intersectStrings(requested.env ?? [], granted.env ?? [])
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
function intersectStrings(a, b) {
|
|
1141
|
+
const setB = new Set(b);
|
|
1142
|
+
return a.filter((item) => setB.has(item));
|
|
1143
|
+
}
|
|
1144
|
+
function intersectNumbers(a, b) {
|
|
1145
|
+
const setB = new Set(b);
|
|
1146
|
+
return a.filter((item) => setB.has(item));
|
|
1147
|
+
}
|
|
1148
|
+
function buildSandboxEnv(permissions) {
|
|
1149
|
+
const env = {
|
|
1150
|
+
// Always pass NODE_ENV and PATH
|
|
1151
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
1152
|
+
PATH: process.env.PATH,
|
|
1153
|
+
// Debug flag
|
|
1154
|
+
VOLE_LOG_LEVEL: process.env.VOLE_LOG_LEVEL
|
|
1155
|
+
};
|
|
1156
|
+
if (permissions.listen.length > 0) {
|
|
1157
|
+
env.VOLE_LISTEN_PORTS = permissions.listen.join(",");
|
|
1158
|
+
}
|
|
1159
|
+
for (const key of permissions.env) {
|
|
1160
|
+
if (process.env[key] !== void 0) {
|
|
1161
|
+
env[key] = process.env[key];
|
|
1162
|
+
} else {
|
|
1163
|
+
logger9.warn(`Env var "${key}" is permitted but not set in environment`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return env;
|
|
1167
|
+
}
|
|
1168
|
+
function validatePermissions(manifest, config) {
|
|
1169
|
+
const warnings = [];
|
|
1170
|
+
const effective = computeEffectivePermissions(manifest, config);
|
|
1171
|
+
const requested = manifest.permissions ?? {};
|
|
1172
|
+
for (const domain of requested.network ?? []) {
|
|
1173
|
+
if (!effective.network.includes(domain)) {
|
|
1174
|
+
warnings.push(
|
|
1175
|
+
`Network access to "${domain}" requested by ${manifest.name} but not granted in config`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
for (const port of requested.listen ?? []) {
|
|
1180
|
+
if (!effective.listen.includes(port)) {
|
|
1181
|
+
warnings.push(
|
|
1182
|
+
`Listen on port ${port} requested by ${manifest.name} but not granted in config`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
for (const fspath of requested.filesystem ?? []) {
|
|
1187
|
+
if (!effective.filesystem.includes(fspath)) {
|
|
1188
|
+
warnings.push(
|
|
1189
|
+
`Filesystem access to "${fspath}" requested by ${manifest.name} but not granted in config`
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
for (const envVar of requested.env ?? []) {
|
|
1194
|
+
if (!effective.env.includes(envVar)) {
|
|
1195
|
+
warnings.push(
|
|
1196
|
+
`Env var "${envVar}" requested by ${manifest.name} but not granted in config`
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (warnings.length > 0) {
|
|
1201
|
+
for (const w of warnings) {
|
|
1202
|
+
logger9.info(w);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return warnings;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/paw/loader.ts
|
|
1209
|
+
var logger10 = createLogger("paw-loader");
|
|
1210
|
+
async function loadInProcessPaw(pawPath, manifest, config) {
|
|
1211
|
+
const entryPath = path3.resolve(pawPath, manifest.entry);
|
|
1212
|
+
logger10.info(`Loading in-process Paw "${manifest.name}" from ${entryPath}`);
|
|
1213
|
+
const module = await import(entryPath);
|
|
1214
|
+
const definition = module.default ?? module;
|
|
1215
|
+
if (definition.onLoad) {
|
|
1216
|
+
await definition.onLoad(config);
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
name: manifest.name,
|
|
1220
|
+
manifest,
|
|
1221
|
+
config,
|
|
1222
|
+
healthy: true,
|
|
1223
|
+
transport: "ipc",
|
|
1224
|
+
inProcess: true,
|
|
1225
|
+
definition
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
async function loadSubprocessPaw(pawPath, manifest, config) {
|
|
1229
|
+
const entryPath = path3.resolve(pawPath, manifest.entry);
|
|
1230
|
+
const transport = manifest.transport ?? "ipc";
|
|
1231
|
+
const permissions = computeEffectivePermissions(manifest, config);
|
|
1232
|
+
const env = buildSandboxEnv(permissions);
|
|
1233
|
+
logger10.info(
|
|
1234
|
+
`Spawning subprocess Paw "${manifest.name}" (transport: ${transport}) from ${entryPath}`
|
|
1235
|
+
);
|
|
1236
|
+
const stdioConfig = transport === "ipc" ? ["pipe", "pipe", "pipe", "ipc"] : ["pipe", "pipe", "pipe"];
|
|
1237
|
+
const child = execa("node", [entryPath], {
|
|
1238
|
+
env,
|
|
1239
|
+
stdio: stdioConfig,
|
|
1240
|
+
reject: false,
|
|
1241
|
+
cleanup: true
|
|
1242
|
+
});
|
|
1243
|
+
child.stderr?.on("data", (data) => {
|
|
1244
|
+
logger10.warn(`[${manifest.name}] ${data.toString().trimEnd()}`);
|
|
1245
|
+
});
|
|
1246
|
+
const ipcTransport = createTransport(transport, child);
|
|
1247
|
+
const instance = {
|
|
1248
|
+
name: manifest.name,
|
|
1249
|
+
manifest,
|
|
1250
|
+
config,
|
|
1251
|
+
healthy: true,
|
|
1252
|
+
transport,
|
|
1253
|
+
inProcess: false,
|
|
1254
|
+
process: {
|
|
1255
|
+
kill: () => child.kill(),
|
|
1256
|
+
pid: child.pid
|
|
1257
|
+
},
|
|
1258
|
+
sendRequest: (method, params) => ipcTransport.request(method, params)
|
|
1259
|
+
};
|
|
1260
|
+
child.then?.((result) => {
|
|
1261
|
+
if (instance.healthy) {
|
|
1262
|
+
instance.healthy = false;
|
|
1263
|
+
logger10.error(
|
|
1264
|
+
`Paw "${manifest.name}" exited unexpectedly (code: ${result.exitCode})`
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
}).catch?.(() => {
|
|
1268
|
+
instance.healthy = false;
|
|
1269
|
+
});
|
|
1270
|
+
return { instance, transport: ipcTransport };
|
|
1271
|
+
}
|
|
1272
|
+
async function shutdownPaw(instance) {
|
|
1273
|
+
if (instance.inProcess) {
|
|
1274
|
+
if (instance.definition?.onUnload) {
|
|
1275
|
+
await instance.definition.onUnload();
|
|
1276
|
+
}
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (instance.sendRequest) {
|
|
1280
|
+
try {
|
|
1281
|
+
await Promise.race([
|
|
1282
|
+
instance.sendRequest("shutdown"),
|
|
1283
|
+
new Promise((resolve6) => setTimeout(resolve6, 5e3))
|
|
1284
|
+
]);
|
|
1285
|
+
} catch {
|
|
1286
|
+
logger10.warn(`Shutdown request to "${instance.name}" failed, killing process`);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
instance.process?.kill();
|
|
1290
|
+
instance.healthy = false;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// src/paw/registry.ts
|
|
1294
|
+
var logger11 = createLogger("paw-registry");
|
|
1295
|
+
var PawRegistry = class {
|
|
1296
|
+
constructor(bus, toolRegistry, projectRoot) {
|
|
1297
|
+
this.bus = bus;
|
|
1298
|
+
this.toolRegistry = toolRegistry;
|
|
1299
|
+
this.projectRoot = projectRoot;
|
|
1300
|
+
this.bus.on("paw:crashed", ({ pawName }) => {
|
|
1301
|
+
const instance = this.paws.get(pawName);
|
|
1302
|
+
if (instance) {
|
|
1303
|
+
instance.healthy = false;
|
|
1304
|
+
this.toolRegistry.unregister(pawName);
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
paws = /* @__PURE__ */ new Map();
|
|
1309
|
+
transports = /* @__PURE__ */ new Map();
|
|
1310
|
+
perceiveHooks = [];
|
|
1311
|
+
observeHookPaws = [];
|
|
1312
|
+
bootstrapPaws = [];
|
|
1313
|
+
compactPaws = [];
|
|
1314
|
+
brainPawName;
|
|
1315
|
+
/** Maps config path → manifest name (e.g. "./paws/paw-ollama" → "@openvole/paw-ollama") */
|
|
1316
|
+
configToManifest = /* @__PURE__ */ new Map();
|
|
1317
|
+
skillRegistry;
|
|
1318
|
+
taskQueue;
|
|
1319
|
+
scheduler;
|
|
1320
|
+
/** Inject queryable registries (called after construction to avoid circular deps) */
|
|
1321
|
+
setQuerySources(skills, tasks, scheduler) {
|
|
1322
|
+
this.skillRegistry = skills;
|
|
1323
|
+
this.taskQueue = tasks;
|
|
1324
|
+
this.scheduler = scheduler;
|
|
1325
|
+
}
|
|
1326
|
+
/** Load and register a Paw */
|
|
1327
|
+
async load(config) {
|
|
1328
|
+
const pawPath = resolvePawPath(config.name, this.projectRoot);
|
|
1329
|
+
const manifest = await readPawManifest(pawPath);
|
|
1330
|
+
if (!manifest) {
|
|
1331
|
+
logger11.error(`Failed to read manifest for "${config.name}"`);
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
const pawName = manifest.name;
|
|
1335
|
+
if (this.paws.has(pawName)) {
|
|
1336
|
+
logger11.warn(`Paw "${pawName}" is already loaded`);
|
|
1337
|
+
return false;
|
|
1338
|
+
}
|
|
1339
|
+
this.configToManifest.set(config.name, pawName);
|
|
1340
|
+
validatePermissions(manifest, config);
|
|
1341
|
+
try {
|
|
1342
|
+
let instance;
|
|
1343
|
+
if (manifest.inProcess) {
|
|
1344
|
+
instance = await loadInProcessPaw(pawPath, manifest, config);
|
|
1345
|
+
this.registerInProcessTools(instance);
|
|
1346
|
+
} else {
|
|
1347
|
+
const result = await loadSubprocessPaw(pawPath, manifest, config);
|
|
1348
|
+
instance = result.instance;
|
|
1349
|
+
this.transports.set(pawName, result.transport);
|
|
1350
|
+
this.setupTransportHandlers(pawName, result.transport);
|
|
1351
|
+
await this.waitForRegistration(pawName, result.transport);
|
|
1352
|
+
}
|
|
1353
|
+
this.paws.set(pawName, instance);
|
|
1354
|
+
if (instance.definition?.hooks?.onPerceive) {
|
|
1355
|
+
const hookConfig = config.hooks?.perceive;
|
|
1356
|
+
const hasTools = (instance.definition?.tools?.length ?? 0) > 0 || manifest.tools.length > 0;
|
|
1357
|
+
this.perceiveHooks.push({
|
|
1358
|
+
pawName,
|
|
1359
|
+
order: hookConfig?.order ?? 100,
|
|
1360
|
+
pipeline: hookConfig?.pipeline ?? true,
|
|
1361
|
+
hasTools
|
|
1362
|
+
});
|
|
1363
|
+
this.perceiveHooks.sort((a, b) => a.order - b.order);
|
|
1364
|
+
}
|
|
1365
|
+
if (instance.definition?.hooks?.onObserve) {
|
|
1366
|
+
this.observeHookPaws.push(pawName);
|
|
1367
|
+
}
|
|
1368
|
+
if (instance.definition?.hooks?.onBootstrap) {
|
|
1369
|
+
this.bootstrapPaws.push(pawName);
|
|
1370
|
+
}
|
|
1371
|
+
if (instance.definition?.hooks?.onCompact) {
|
|
1372
|
+
this.compactPaws.push(pawName);
|
|
1373
|
+
}
|
|
1374
|
+
logger11.info(`Paw "${pawName}" loaded successfully`);
|
|
1375
|
+
this.bus.emit("paw:registered", { pawName });
|
|
1376
|
+
return true;
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
logger11.error(`Failed to load Paw "${pawName}": ${err}`);
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
/** Unload a Paw (accepts config path or manifest name) */
|
|
1383
|
+
async unload(name) {
|
|
1384
|
+
const pawName = this.configToManifest.get(name) ?? name;
|
|
1385
|
+
const instance = this.paws.get(pawName);
|
|
1386
|
+
if (!instance) {
|
|
1387
|
+
logger11.warn(`Paw "${pawName}" is not loaded`);
|
|
1388
|
+
return false;
|
|
1389
|
+
}
|
|
1390
|
+
name = pawName;
|
|
1391
|
+
await shutdownPaw(instance);
|
|
1392
|
+
const transport = this.transports.get(name);
|
|
1393
|
+
if (transport) {
|
|
1394
|
+
transport.dispose();
|
|
1395
|
+
this.transports.delete(name);
|
|
1396
|
+
}
|
|
1397
|
+
this.toolRegistry.unregister(name);
|
|
1398
|
+
this.perceiveHooks = this.perceiveHooks.filter((h) => h.pawName !== name);
|
|
1399
|
+
this.observeHookPaws = this.observeHookPaws.filter((n) => n !== name);
|
|
1400
|
+
this.bootstrapPaws = this.bootstrapPaws.filter((n) => n !== name);
|
|
1401
|
+
this.compactPaws = this.compactPaws.filter((n) => n !== name);
|
|
1402
|
+
this.paws.delete(name);
|
|
1403
|
+
for (const [configName, manifestName] of this.configToManifest) {
|
|
1404
|
+
if (manifestName === name) {
|
|
1405
|
+
this.configToManifest.delete(configName);
|
|
1406
|
+
break;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
logger11.info(`Paw "${name}" unloaded`);
|
|
1410
|
+
this.bus.emit("paw:unregistered", { pawName: name });
|
|
1411
|
+
return true;
|
|
1412
|
+
}
|
|
1413
|
+
/** Resolve a config name to its manifest name */
|
|
1414
|
+
resolveManifestName(configName) {
|
|
1415
|
+
return this.configToManifest.get(configName) ?? configName;
|
|
1416
|
+
}
|
|
1417
|
+
/** Set the Brain Paw name (accepts config path or manifest name) */
|
|
1418
|
+
setBrain(name) {
|
|
1419
|
+
this.brainPawName = this.configToManifest.get(name) ?? name;
|
|
1420
|
+
}
|
|
1421
|
+
/** Get the Brain Paw name */
|
|
1422
|
+
getBrainName() {
|
|
1423
|
+
return this.brainPawName;
|
|
1424
|
+
}
|
|
1425
|
+
/** Get a Paw instance */
|
|
1426
|
+
get(name) {
|
|
1427
|
+
return this.paws.get(name);
|
|
1428
|
+
}
|
|
1429
|
+
/** List all loaded Paws */
|
|
1430
|
+
list() {
|
|
1431
|
+
return Array.from(this.paws.values());
|
|
1432
|
+
}
|
|
1433
|
+
/** Check if a Paw is healthy */
|
|
1434
|
+
isHealthy(name) {
|
|
1435
|
+
return this.paws.get(name)?.healthy ?? false;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Run GLOBAL perceive hooks — only Paws without tools.
|
|
1439
|
+
* Paws with tools use lazy perceive (called just before their tool executes).
|
|
1440
|
+
*/
|
|
1441
|
+
async runGlobalPerceiveHooks(context) {
|
|
1442
|
+
let chainedContext = { ...context };
|
|
1443
|
+
const globalChained = this.perceiveHooks.filter((h) => h.pipeline && !h.hasTools);
|
|
1444
|
+
const globalUnchained = this.perceiveHooks.filter((h) => !h.pipeline && !h.hasTools);
|
|
1445
|
+
for (const hook of globalChained) {
|
|
1446
|
+
const instance = this.paws.get(hook.pawName);
|
|
1447
|
+
if (!instance?.healthy) continue;
|
|
1448
|
+
try {
|
|
1449
|
+
chainedContext = await this.callPerceive(hook.pawName, chainedContext);
|
|
1450
|
+
} catch (err) {
|
|
1451
|
+
logger11.error(`Perceive hook error from "${hook.pawName}": ${err}`);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
if (globalUnchained.length > 0) {
|
|
1455
|
+
const results = await Promise.allSettled(
|
|
1456
|
+
globalUnchained.map(async (hook) => {
|
|
1457
|
+
const instance = this.paws.get(hook.pawName);
|
|
1458
|
+
if (!instance?.healthy) return null;
|
|
1459
|
+
try {
|
|
1460
|
+
return await this.callPerceive(hook.pawName, context);
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
logger11.error(`Unchained perceive error from "${hook.pawName}": ${err}`);
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
})
|
|
1466
|
+
);
|
|
1467
|
+
for (const result of results) {
|
|
1468
|
+
if (result.status === "fulfilled" && result.value) {
|
|
1469
|
+
Object.assign(chainedContext.metadata, result.value.metadata);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return chainedContext;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Run LAZY perceive for a specific Paw — called just before its tool executes.
|
|
1477
|
+
* Only runs if the Paw has an onPerceive hook registered.
|
|
1478
|
+
*/
|
|
1479
|
+
async runLazyPerceive(pawName, context) {
|
|
1480
|
+
const hook = this.perceiveHooks.find((h) => h.pawName === pawName && h.hasTools);
|
|
1481
|
+
if (!hook) return context;
|
|
1482
|
+
const instance = this.paws.get(pawName);
|
|
1483
|
+
if (!instance?.healthy) return context;
|
|
1484
|
+
try {
|
|
1485
|
+
logger11.debug(`Lazy perceive for "${pawName}" before tool execution`);
|
|
1486
|
+
return await this.callPerceive(pawName, context);
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
logger11.error(`Lazy perceive error from "${pawName}": ${err}`);
|
|
1489
|
+
return context;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
/** Run all Observe hooks concurrently (fire-and-forget) */
|
|
1493
|
+
runObserveHooks(result) {
|
|
1494
|
+
for (const pawName of this.observeHookPaws) {
|
|
1495
|
+
const instance = this.paws.get(pawName);
|
|
1496
|
+
if (!instance?.healthy) continue;
|
|
1497
|
+
this.callObserve(pawName, result).catch((err) => {
|
|
1498
|
+
logger11.error(`Observe hook error from "${pawName}": ${err}`);
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
/** Run bootstrap hooks — called once at the start of a task */
|
|
1503
|
+
async runBootstrapHooks(context) {
|
|
1504
|
+
let ctx = { ...context };
|
|
1505
|
+
for (const pawName of this.bootstrapPaws) {
|
|
1506
|
+
const instance = this.paws.get(pawName);
|
|
1507
|
+
if (!instance?.healthy) continue;
|
|
1508
|
+
try {
|
|
1509
|
+
if (instance.inProcess && instance.definition?.hooks?.onBootstrap) {
|
|
1510
|
+
ctx = await instance.definition.hooks.onBootstrap(ctx);
|
|
1511
|
+
} else {
|
|
1512
|
+
const transport = this.transports.get(pawName);
|
|
1513
|
+
if (transport) {
|
|
1514
|
+
ctx = await transport.request("bootstrap", ctx);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
logger11.error(`Bootstrap hook error from "${pawName}": ${err}`);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return ctx;
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Run compact hooks — called when context exceeds size threshold.
|
|
1525
|
+
* Paws can compress/summarize messages to free up context window space.
|
|
1526
|
+
*/
|
|
1527
|
+
async runCompactHooks(context) {
|
|
1528
|
+
let ctx = { ...context };
|
|
1529
|
+
for (const pawName of this.compactPaws) {
|
|
1530
|
+
const instance = this.paws.get(pawName);
|
|
1531
|
+
if (!instance?.healthy) continue;
|
|
1532
|
+
try {
|
|
1533
|
+
if (instance.inProcess && instance.definition?.hooks?.onCompact) {
|
|
1534
|
+
ctx = await instance.definition.hooks.onCompact(ctx);
|
|
1535
|
+
} else {
|
|
1536
|
+
const transport = this.transports.get(pawName);
|
|
1537
|
+
if (transport) {
|
|
1538
|
+
ctx = await transport.request("compact", ctx);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
logger11.info(`Compact hook from "${pawName}" reduced messages from ${context.messages.length} to ${ctx.messages.length}`);
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
logger11.error(`Compact hook error from "${pawName}": ${err}`);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return ctx;
|
|
1547
|
+
}
|
|
1548
|
+
/** Call the Brain Paw's think function */
|
|
1549
|
+
async think(context) {
|
|
1550
|
+
if (!this.brainPawName) return null;
|
|
1551
|
+
const instance = this.paws.get(this.brainPawName);
|
|
1552
|
+
if (!instance?.healthy) {
|
|
1553
|
+
logger11.error(`Brain Paw "${this.brainPawName}" is not healthy`);
|
|
1554
|
+
return null;
|
|
1555
|
+
}
|
|
1556
|
+
if (instance.inProcess && instance.definition?.think) {
|
|
1557
|
+
return instance.definition.think(context);
|
|
1558
|
+
}
|
|
1559
|
+
const transport = this.transports.get(this.brainPawName);
|
|
1560
|
+
if (!transport) {
|
|
1561
|
+
logger11.error(`No transport for Brain Paw "${this.brainPawName}"`);
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
return await transport.request("think", context);
|
|
1565
|
+
}
|
|
1566
|
+
/** Execute a tool on a subprocess Paw */
|
|
1567
|
+
async executeRemoteTool(pawName, toolName, params) {
|
|
1568
|
+
const transport = this.transports.get(pawName);
|
|
1569
|
+
if (!transport) {
|
|
1570
|
+
throw new Error(`No transport for Paw "${pawName}"`);
|
|
1571
|
+
}
|
|
1572
|
+
return transport.request("execute_tool", { toolName, params });
|
|
1573
|
+
}
|
|
1574
|
+
async callPerceive(pawName, context) {
|
|
1575
|
+
const instance = this.paws.get(pawName);
|
|
1576
|
+
if (instance.inProcess && instance.definition?.hooks?.onPerceive) {
|
|
1577
|
+
return instance.definition.hooks.onPerceive(context);
|
|
1578
|
+
}
|
|
1579
|
+
const transport = this.transports.get(pawName);
|
|
1580
|
+
if (transport) {
|
|
1581
|
+
return await transport.request("perceive", context);
|
|
1582
|
+
}
|
|
1583
|
+
return context;
|
|
1584
|
+
}
|
|
1585
|
+
async callObserve(pawName, result) {
|
|
1586
|
+
const instance = this.paws.get(pawName);
|
|
1587
|
+
if (instance.inProcess && instance.definition?.hooks?.onObserve) {
|
|
1588
|
+
await instance.definition.hooks.onObserve(result);
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
const transport = this.transports.get(pawName);
|
|
1592
|
+
if (transport) {
|
|
1593
|
+
await transport.request("observe", result);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
registerInProcessTools(instance) {
|
|
1597
|
+
if (instance.definition?.tools) {
|
|
1598
|
+
this.toolRegistry.register(
|
|
1599
|
+
instance.name,
|
|
1600
|
+
instance.definition.tools,
|
|
1601
|
+
true
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
setupTransportHandlers(pawName, transport) {
|
|
1606
|
+
transport.onRequest("log", async (params) => {
|
|
1607
|
+
const { level, message } = params;
|
|
1608
|
+
const prefix = `[${pawName}]`;
|
|
1609
|
+
switch (level) {
|
|
1610
|
+
case "error":
|
|
1611
|
+
console.error(prefix, message);
|
|
1612
|
+
break;
|
|
1613
|
+
case "warn":
|
|
1614
|
+
console.warn(prefix, message);
|
|
1615
|
+
break;
|
|
1616
|
+
case "info":
|
|
1617
|
+
console.info(prefix, message);
|
|
1618
|
+
break;
|
|
1619
|
+
default:
|
|
1620
|
+
console.debug(prefix, message);
|
|
1621
|
+
}
|
|
1622
|
+
return { ok: true };
|
|
1623
|
+
});
|
|
1624
|
+
transport.onRequest("emit", async (params) => {
|
|
1625
|
+
const { event } = params;
|
|
1626
|
+
logger11.info(`Paw "${pawName}" emitted event: ${event}`);
|
|
1627
|
+
return { ok: true };
|
|
1628
|
+
});
|
|
1629
|
+
transport.onRequest("subscribe", async (params) => {
|
|
1630
|
+
const { events } = params;
|
|
1631
|
+
const instance = this.paws.get(pawName);
|
|
1632
|
+
if (instance) {
|
|
1633
|
+
instance.subscriptions = events;
|
|
1634
|
+
this.setupBusForwarding(pawName, events, transport);
|
|
1635
|
+
logger11.info(`Paw "${pawName}" subscribed to events: ${events.join(", ")}`);
|
|
1636
|
+
}
|
|
1637
|
+
return { ok: true };
|
|
1638
|
+
});
|
|
1639
|
+
transport.onRequest("query", async (params) => {
|
|
1640
|
+
const { type } = params;
|
|
1641
|
+
return this.handleQuery(type);
|
|
1642
|
+
});
|
|
1643
|
+
transport.onRequest("create_task", async (params) => {
|
|
1644
|
+
const { input, source, sessionId, metadata } = params;
|
|
1645
|
+
if (!this.taskQueue) {
|
|
1646
|
+
return { error: "Task queue not available" };
|
|
1647
|
+
}
|
|
1648
|
+
const task = this.taskQueue.enqueue(input, source ?? "paw", { sessionId, metadata });
|
|
1649
|
+
logger11.info(`Paw "${pawName}" created task ${task.id}: "${input.substring(0, 50)}"`);
|
|
1650
|
+
return { taskId: task.id };
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
/** Forward bus events to a Paw that subscribed */
|
|
1654
|
+
setupBusForwarding(pawName, events, transport) {
|
|
1655
|
+
for (const eventName of events) {
|
|
1656
|
+
this.bus.on(eventName, (data) => {
|
|
1657
|
+
const instance = this.paws.get(pawName);
|
|
1658
|
+
if (!instance?.healthy) return;
|
|
1659
|
+
transport.notify("bus_event", { event: eventName, data });
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
/** Handle state queries from Paws */
|
|
1664
|
+
handleQuery(type) {
|
|
1665
|
+
switch (type) {
|
|
1666
|
+
case "tools":
|
|
1667
|
+
return this.toolRegistry.list().map((t) => ({
|
|
1668
|
+
name: t.name,
|
|
1669
|
+
description: t.description,
|
|
1670
|
+
pawName: t.pawName,
|
|
1671
|
+
inProcess: t.inProcess
|
|
1672
|
+
}));
|
|
1673
|
+
case "paws":
|
|
1674
|
+
return Array.from(this.paws.values()).map((p) => ({
|
|
1675
|
+
name: p.name,
|
|
1676
|
+
healthy: p.healthy,
|
|
1677
|
+
inProcess: p.inProcess,
|
|
1678
|
+
transport: p.transport,
|
|
1679
|
+
toolCount: this.toolRegistry.toolsForPaw(p.name).length
|
|
1680
|
+
}));
|
|
1681
|
+
case "skills":
|
|
1682
|
+
return this.skillRegistry?.list().map((s) => ({
|
|
1683
|
+
name: s.name,
|
|
1684
|
+
active: s.active,
|
|
1685
|
+
missingTools: s.missingTools,
|
|
1686
|
+
description: s.definition.description
|
|
1687
|
+
})) ?? [];
|
|
1688
|
+
case "tasks":
|
|
1689
|
+
return this.taskQueue?.list().map((t) => ({
|
|
1690
|
+
id: t.id,
|
|
1691
|
+
source: t.source,
|
|
1692
|
+
input: t.input,
|
|
1693
|
+
status: t.status,
|
|
1694
|
+
createdAt: t.createdAt,
|
|
1695
|
+
startedAt: t.startedAt,
|
|
1696
|
+
completedAt: t.completedAt
|
|
1697
|
+
})) ?? [];
|
|
1698
|
+
case "schedules":
|
|
1699
|
+
return this.scheduler?.list() ?? [];
|
|
1700
|
+
default:
|
|
1701
|
+
return { error: `Unknown query type: ${type}` };
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
async waitForRegistration(pawName, transport) {
|
|
1705
|
+
return new Promise((resolve6, reject) => {
|
|
1706
|
+
const timeout = setTimeout(() => {
|
|
1707
|
+
reject(new Error(`Paw "${pawName}" did not register within 10 seconds`));
|
|
1708
|
+
}, 1e4);
|
|
1709
|
+
transport.onRequest("register", async (params) => {
|
|
1710
|
+
clearTimeout(timeout);
|
|
1711
|
+
const registration = params;
|
|
1712
|
+
if (registration.tools) {
|
|
1713
|
+
const toolDefs = registration.tools.map((t) => ({
|
|
1714
|
+
name: t.name,
|
|
1715
|
+
description: t.description,
|
|
1716
|
+
parameters: {},
|
|
1717
|
+
// Schema validated on Paw side
|
|
1718
|
+
execute: async (toolParams) => this.executeRemoteTool(pawName, t.name, toolParams)
|
|
1719
|
+
}));
|
|
1720
|
+
this.toolRegistry.register(pawName, toolDefs, false);
|
|
1721
|
+
}
|
|
1722
|
+
logger11.info(`Paw "${pawName}" registered with ${registration.tools?.length ?? 0} tools`);
|
|
1723
|
+
resolve6();
|
|
1724
|
+
return { ok: true };
|
|
1725
|
+
});
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
|
|
1730
|
+
// src/skill/registry.ts
|
|
1731
|
+
import * as fs4 from "fs/promises";
|
|
1732
|
+
import * as path5 from "path";
|
|
1733
|
+
|
|
1734
|
+
// src/skill/loader.ts
|
|
1735
|
+
import * as fs3 from "fs/promises";
|
|
1736
|
+
import * as path4 from "path";
|
|
1737
|
+
import { parse as parseYaml } from "yaml";
|
|
1738
|
+
var logger12 = createLogger("skill-loader");
|
|
1739
|
+
async function loadSkillFromDirectory(skillDir) {
|
|
1740
|
+
const skillMdPath = path4.join(skillDir, "SKILL.md");
|
|
1741
|
+
try {
|
|
1742
|
+
const raw = await fs3.readFile(skillMdPath, "utf-8");
|
|
1743
|
+
return parseSkillMd(raw, skillMdPath);
|
|
1744
|
+
} catch (err) {
|
|
1745
|
+
if (err.code === "ENOENT") {
|
|
1746
|
+
logger12.error("SKILL.md not found: %s", skillMdPath);
|
|
1747
|
+
} else {
|
|
1748
|
+
logger12.error("Failed to read SKILL.md at %s: %s", skillMdPath, err);
|
|
1749
|
+
}
|
|
1750
|
+
return null;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
function parseSkillMd(content, filePath) {
|
|
1754
|
+
const { frontmatter, body } = extractFrontmatter(content);
|
|
1755
|
+
if (!frontmatter) {
|
|
1756
|
+
logger12.error("No YAML frontmatter found in %s", filePath);
|
|
1757
|
+
return null;
|
|
1758
|
+
}
|
|
1759
|
+
let meta;
|
|
1760
|
+
try {
|
|
1761
|
+
meta = parseYaml(frontmatter);
|
|
1762
|
+
} catch (err) {
|
|
1763
|
+
logger12.error("Invalid YAML frontmatter in %s: %s", filePath, err);
|
|
1764
|
+
return null;
|
|
1765
|
+
}
|
|
1766
|
+
if (!meta.name || typeof meta.name !== "string") {
|
|
1767
|
+
logger12.error('SKILL.md missing "name" in frontmatter: %s', filePath);
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
if (!meta.description || typeof meta.description !== "string") {
|
|
1771
|
+
logger12.error('SKILL.md missing "description" in frontmatter: %s', filePath);
|
|
1772
|
+
return null;
|
|
1773
|
+
}
|
|
1774
|
+
const instructions = body.trim();
|
|
1775
|
+
if (!instructions) {
|
|
1776
|
+
logger12.error("SKILL.md has no instructions (empty body): %s", filePath);
|
|
1777
|
+
return null;
|
|
1778
|
+
}
|
|
1779
|
+
const openclaw = extractOpenClawMetadata(meta);
|
|
1780
|
+
return {
|
|
1781
|
+
name: meta.name,
|
|
1782
|
+
description: meta.description,
|
|
1783
|
+
version: typeof meta.version === "string" ? meta.version : typeof meta.version === "number" ? String(meta.version) : void 0,
|
|
1784
|
+
requiredTools: toStringArray(meta.requiredTools),
|
|
1785
|
+
optionalTools: toStringArray(meta.optionalTools),
|
|
1786
|
+
instructions,
|
|
1787
|
+
tags: toStringArray(meta.tags),
|
|
1788
|
+
// OpenClaw compatibility fields
|
|
1789
|
+
requires: openclaw.requires
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
function extractOpenClawMetadata(meta) {
|
|
1793
|
+
const metadata = meta.metadata;
|
|
1794
|
+
if (!metadata) return { requires: void 0 };
|
|
1795
|
+
const oc = metadata.openclaw ?? metadata.clawdbot;
|
|
1796
|
+
if (!oc) return { requires: void 0 };
|
|
1797
|
+
const req = oc.requires;
|
|
1798
|
+
if (!req) return { requires: void 0 };
|
|
1799
|
+
return {
|
|
1800
|
+
requires: {
|
|
1801
|
+
env: toStringArray(req.env),
|
|
1802
|
+
bins: toStringArray(req.bins),
|
|
1803
|
+
anyBins: toStringArray(req.anyBins)
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
function extractFrontmatter(content) {
|
|
1808
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
1809
|
+
if (!match) {
|
|
1810
|
+
return { frontmatter: null, body: content };
|
|
1811
|
+
}
|
|
1812
|
+
return { frontmatter: match[1], body: match[2] };
|
|
1813
|
+
}
|
|
1814
|
+
function toStringArray(value) {
|
|
1815
|
+
if (!value) return [];
|
|
1816
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
1817
|
+
return [];
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// src/skill/registry.ts
|
|
1821
|
+
var logger13 = createLogger("skill-registry");
|
|
1822
|
+
async function resolveSkillPath(name, projectRoot) {
|
|
1823
|
+
if (name.startsWith(".") || name.startsWith("/")) {
|
|
1824
|
+
return path5.resolve(projectRoot, name);
|
|
1825
|
+
}
|
|
1826
|
+
if (name.startsWith("clawhub/")) {
|
|
1827
|
+
return path5.resolve(projectRoot, ".openvole", "skills", name);
|
|
1828
|
+
}
|
|
1829
|
+
const localPath = path5.resolve(projectRoot, ".openvole", "skills", name);
|
|
1830
|
+
if (await exists(path5.join(localPath, "SKILL.md"))) return localPath;
|
|
1831
|
+
const clawHubPath = path5.resolve(projectRoot, ".openvole", "skills", "clawhub", name);
|
|
1832
|
+
if (await exists(path5.join(clawHubPath, "SKILL.md"))) return clawHubPath;
|
|
1833
|
+
return path5.resolve(projectRoot, "node_modules", name);
|
|
1834
|
+
}
|
|
1835
|
+
async function exists(filePath) {
|
|
1836
|
+
try {
|
|
1837
|
+
await fs4.access(filePath);
|
|
1838
|
+
return true;
|
|
1839
|
+
} catch {
|
|
1840
|
+
return false;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
var SkillRegistry = class {
|
|
1844
|
+
constructor(bus, toolRegistry, projectRoot) {
|
|
1845
|
+
this.bus = bus;
|
|
1846
|
+
this.toolRegistry = toolRegistry;
|
|
1847
|
+
this.projectRoot = projectRoot;
|
|
1848
|
+
this.bus.on("tool:registered", () => this.resolve());
|
|
1849
|
+
this.bus.on("tool:unregistered", () => this.resolve());
|
|
1850
|
+
}
|
|
1851
|
+
skills = /* @__PURE__ */ new Map();
|
|
1852
|
+
/** Load a Skill from a directory containing SKILL.md */
|
|
1853
|
+
async load(nameOrPath) {
|
|
1854
|
+
const skillDir = await resolveSkillPath(nameOrPath, this.projectRoot);
|
|
1855
|
+
const definition = await loadSkillFromDirectory(skillDir);
|
|
1856
|
+
if (!definition) {
|
|
1857
|
+
return false;
|
|
1858
|
+
}
|
|
1859
|
+
const registryKey = nameOrPath.startsWith(".") || nameOrPath.startsWith("/") ? definition.name : nameOrPath;
|
|
1860
|
+
if (this.skills.has(registryKey)) {
|
|
1861
|
+
logger13.warn(`Skill "${registryKey}" is already loaded`);
|
|
1862
|
+
return false;
|
|
1863
|
+
}
|
|
1864
|
+
const instance = {
|
|
1865
|
+
name: registryKey,
|
|
1866
|
+
definition,
|
|
1867
|
+
path: skillDir,
|
|
1868
|
+
active: false,
|
|
1869
|
+
missingTools: [...definition.requiredTools]
|
|
1870
|
+
};
|
|
1871
|
+
this.skills.set(registryKey, instance);
|
|
1872
|
+
logger13.info(`Skill "${registryKey}" loaded from ${skillDir}`);
|
|
1873
|
+
this.resolve();
|
|
1874
|
+
return true;
|
|
1875
|
+
}
|
|
1876
|
+
/** Unload a Skill */
|
|
1877
|
+
unload(name) {
|
|
1878
|
+
if (!this.skills.has(name)) {
|
|
1879
|
+
return false;
|
|
1880
|
+
}
|
|
1881
|
+
this.skills.delete(name);
|
|
1882
|
+
logger13.info(`Skill "${name}" unloaded`);
|
|
1883
|
+
return true;
|
|
1884
|
+
}
|
|
1885
|
+
/** Re-run the resolver against the current tool registry */
|
|
1886
|
+
resolve() {
|
|
1887
|
+
resolveSkills(Array.from(this.skills.values()), this.toolRegistry);
|
|
1888
|
+
}
|
|
1889
|
+
/** Get all Skill instances */
|
|
1890
|
+
list() {
|
|
1891
|
+
return Array.from(this.skills.values());
|
|
1892
|
+
}
|
|
1893
|
+
/** Get active Skills only */
|
|
1894
|
+
active() {
|
|
1895
|
+
return this.list().filter((s) => s.active);
|
|
1896
|
+
}
|
|
1897
|
+
/** Get a Skill by name */
|
|
1898
|
+
get(name) {
|
|
1899
|
+
return this.skills.get(name);
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
|
|
1903
|
+
// src/io/tty.ts
|
|
1904
|
+
import * as readline from "readline";
|
|
1905
|
+
function createTtyIO() {
|
|
1906
|
+
return {
|
|
1907
|
+
async confirm(message) {
|
|
1908
|
+
const rl = readline.createInterface({
|
|
1909
|
+
input: process.stdin,
|
|
1910
|
+
output: process.stdout
|
|
1911
|
+
});
|
|
1912
|
+
try {
|
|
1913
|
+
return await new Promise((resolve6) => {
|
|
1914
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
1915
|
+
resolve6(answer.trim().toLowerCase() === "y");
|
|
1916
|
+
});
|
|
1917
|
+
});
|
|
1918
|
+
} finally {
|
|
1919
|
+
rl.close();
|
|
1920
|
+
}
|
|
1921
|
+
},
|
|
1922
|
+
async prompt(message) {
|
|
1923
|
+
const rl = readline.createInterface({
|
|
1924
|
+
input: process.stdin,
|
|
1925
|
+
output: process.stdout
|
|
1926
|
+
});
|
|
1927
|
+
try {
|
|
1928
|
+
return await new Promise((resolve6) => {
|
|
1929
|
+
rl.question(`${message} `, (answer) => {
|
|
1930
|
+
resolve6(answer.trim());
|
|
1931
|
+
});
|
|
1932
|
+
});
|
|
1933
|
+
} finally {
|
|
1934
|
+
rl.close();
|
|
1935
|
+
}
|
|
1936
|
+
},
|
|
1937
|
+
notify(message) {
|
|
1938
|
+
process.stdout.write(`${message}
|
|
1939
|
+
`);
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// src/index.ts
|
|
1945
|
+
import * as path7 from "path";
|
|
1946
|
+
import * as fs6 from "fs/promises";
|
|
1947
|
+
|
|
1948
|
+
// src/tool/core-tools.ts
|
|
1949
|
+
import * as fs5 from "fs/promises";
|
|
1950
|
+
import * as path6 from "path";
|
|
1951
|
+
import { z as z2 } from "zod";
|
|
1952
|
+
function createCoreTools(scheduler, taskQueue, projectRoot, skillRegistry) {
|
|
1953
|
+
const heartbeatPath = path6.resolve(projectRoot, "HEARTBEAT.md");
|
|
1954
|
+
return [
|
|
1955
|
+
// === Scheduling tools ===
|
|
1956
|
+
{
|
|
1957
|
+
name: "schedule_task",
|
|
1958
|
+
description: "Create a recurring scheduled task that runs at a fixed interval",
|
|
1959
|
+
parameters: z2.object({
|
|
1960
|
+
id: z2.string().describe("Unique schedule ID (for cancellation)"),
|
|
1961
|
+
input: z2.string().describe("The task input to enqueue each time"),
|
|
1962
|
+
intervalMinutes: z2.number().describe("How often to run (in minutes)")
|
|
1963
|
+
}),
|
|
1964
|
+
async execute(params) {
|
|
1965
|
+
const { id, input, intervalMinutes } = params;
|
|
1966
|
+
scheduler.add(id, input, intervalMinutes, () => {
|
|
1967
|
+
taskQueue.enqueue(input, "schedule");
|
|
1968
|
+
});
|
|
1969
|
+
return { ok: true, id, intervalMinutes };
|
|
1970
|
+
}
|
|
1971
|
+
},
|
|
1972
|
+
{
|
|
1973
|
+
name: "cancel_schedule",
|
|
1974
|
+
description: "Cancel a previously created scheduled task",
|
|
1975
|
+
parameters: z2.object({
|
|
1976
|
+
id: z2.string().describe("Schedule ID to cancel")
|
|
1977
|
+
}),
|
|
1978
|
+
async execute(params) {
|
|
1979
|
+
const { id } = params;
|
|
1980
|
+
const cancelled = scheduler.cancel(id);
|
|
1981
|
+
return { ok: cancelled, id };
|
|
1982
|
+
}
|
|
1983
|
+
},
|
|
1984
|
+
{
|
|
1985
|
+
name: "list_schedules",
|
|
1986
|
+
description: "List all active scheduled tasks",
|
|
1987
|
+
parameters: z2.object({}),
|
|
1988
|
+
async execute() {
|
|
1989
|
+
return scheduler.list();
|
|
1990
|
+
}
|
|
1991
|
+
},
|
|
1992
|
+
// === Heartbeat tools ===
|
|
1993
|
+
{
|
|
1994
|
+
name: "heartbeat_read",
|
|
1995
|
+
description: "Read the HEARTBEAT.md file containing recurring job definitions",
|
|
1996
|
+
parameters: z2.object({}),
|
|
1997
|
+
async execute() {
|
|
1998
|
+
try {
|
|
1999
|
+
const content = await fs5.readFile(heartbeatPath, "utf-8");
|
|
2000
|
+
return { ok: true, content };
|
|
2001
|
+
} catch {
|
|
2002
|
+
return { ok: true, content: "" };
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
},
|
|
2006
|
+
{
|
|
2007
|
+
name: "heartbeat_write",
|
|
2008
|
+
description: "Update the HEARTBEAT.md file with new recurring job definitions",
|
|
2009
|
+
parameters: z2.object({
|
|
2010
|
+
content: z2.string().describe("The full content to write to HEARTBEAT.md")
|
|
2011
|
+
}),
|
|
2012
|
+
async execute(params) {
|
|
2013
|
+
const { content } = params;
|
|
2014
|
+
await fs5.writeFile(heartbeatPath, content, "utf-8");
|
|
2015
|
+
return { ok: true };
|
|
2016
|
+
}
|
|
2017
|
+
},
|
|
2018
|
+
// === Skill tools (on-demand loading) ===
|
|
2019
|
+
{
|
|
2020
|
+
name: "skill_read",
|
|
2021
|
+
description: "Read the full SKILL.md instructions for a skill by name. Use this when a skill is relevant to the current task.",
|
|
2022
|
+
parameters: z2.object({
|
|
2023
|
+
name: z2.string().describe("Skill name to read")
|
|
2024
|
+
}),
|
|
2025
|
+
async execute(params) {
|
|
2026
|
+
const { name } = params;
|
|
2027
|
+
const skill = skillRegistry.get(name);
|
|
2028
|
+
if (!skill) {
|
|
2029
|
+
return { ok: false, error: `Skill "${name}" not found` };
|
|
2030
|
+
}
|
|
2031
|
+
try {
|
|
2032
|
+
const content = await fs5.readFile(
|
|
2033
|
+
path6.join(skill.path, "SKILL.md"),
|
|
2034
|
+
"utf-8"
|
|
2035
|
+
);
|
|
2036
|
+
return { ok: true, name, content };
|
|
2037
|
+
} catch {
|
|
2038
|
+
return { ok: false, error: `Failed to read SKILL.md for "${name}"` };
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
name: "skill_read_reference",
|
|
2044
|
+
description: "Read a reference file from a skill's references/ directory. Use this for API docs, schemas, or detailed guides.",
|
|
2045
|
+
parameters: z2.object({
|
|
2046
|
+
name: z2.string().describe("Skill name"),
|
|
2047
|
+
file: z2.string().describe("File path relative to the skill's references/ directory")
|
|
2048
|
+
}),
|
|
2049
|
+
async execute(params) {
|
|
2050
|
+
const { name, file } = params;
|
|
2051
|
+
const skill = skillRegistry.get(name);
|
|
2052
|
+
if (!skill) {
|
|
2053
|
+
return { ok: false, error: `Skill "${name}" not found` };
|
|
2054
|
+
}
|
|
2055
|
+
const resolved = path6.resolve(skill.path, "references", file);
|
|
2056
|
+
if (!resolved.startsWith(path6.resolve(skill.path, "references"))) {
|
|
2057
|
+
return { ok: false, error: "Invalid file path" };
|
|
2058
|
+
}
|
|
2059
|
+
try {
|
|
2060
|
+
const content = await fs5.readFile(resolved, "utf-8");
|
|
2061
|
+
return { ok: true, name, file, content };
|
|
2062
|
+
} catch {
|
|
2063
|
+
return { ok: false, error: `File not found: references/${file}` };
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
name: "skill_list_files",
|
|
2069
|
+
description: "List all files in a skill's directory including scripts, references, and assets.",
|
|
2070
|
+
parameters: z2.object({
|
|
2071
|
+
name: z2.string().describe("Skill name")
|
|
2072
|
+
}),
|
|
2073
|
+
async execute(params) {
|
|
2074
|
+
const { name } = params;
|
|
2075
|
+
const skill = skillRegistry.get(name);
|
|
2076
|
+
if (!skill) {
|
|
2077
|
+
return { ok: false, error: `Skill "${name}" not found` };
|
|
2078
|
+
}
|
|
2079
|
+
try {
|
|
2080
|
+
const files = await listFilesRecursive(skill.path);
|
|
2081
|
+
return { ok: true, name, files };
|
|
2082
|
+
} catch {
|
|
2083
|
+
return { ok: false, error: `Failed to list files for "${name}"` };
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
];
|
|
2088
|
+
}
|
|
2089
|
+
async function listFilesRecursive(dir, prefix = "") {
|
|
2090
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
2091
|
+
const files = [];
|
|
2092
|
+
for (const entry of entries) {
|
|
2093
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
2094
|
+
if (entry.isDirectory()) {
|
|
2095
|
+
files.push(...await listFilesRecursive(path6.join(dir, entry.name), rel));
|
|
2096
|
+
} else {
|
|
2097
|
+
files.push(rel);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
return files;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// src/index.ts
|
|
2104
|
+
var engineLogger = {
|
|
2105
|
+
info: (msg, ...args) => console.info(`[openvole] ${msg}`, ...args),
|
|
2106
|
+
error: (msg, ...args) => console.error(`[openvole] ${msg}`, ...args)
|
|
2107
|
+
};
|
|
2108
|
+
async function createEngine(projectRoot, options) {
|
|
2109
|
+
const configPath = options?.configPath ?? path7.resolve(projectRoot, "vole.config.ts");
|
|
2110
|
+
const config = await loadConfig(configPath);
|
|
2111
|
+
const bus = createMessageBus();
|
|
2112
|
+
const toolRegistry = new ToolRegistry(bus);
|
|
2113
|
+
const pawRegistry = new PawRegistry(bus, toolRegistry, projectRoot);
|
|
2114
|
+
const skillRegistry = new SkillRegistry(bus, toolRegistry, projectRoot);
|
|
2115
|
+
const io = options?.io ?? createTtyIO();
|
|
2116
|
+
const rateLimiter = new RateLimiter();
|
|
2117
|
+
const taskQueue = new TaskQueue(bus, config.loop.taskConcurrency, rateLimiter, config.loop.rateLimits);
|
|
2118
|
+
const scheduler = new SchedulerStore();
|
|
2119
|
+
const coreTools = createCoreTools(scheduler, taskQueue, projectRoot, skillRegistry);
|
|
2120
|
+
toolRegistry.register("__core__", coreTools, true);
|
|
2121
|
+
pawRegistry.setQuerySources(skillRegistry, taskQueue, scheduler);
|
|
2122
|
+
taskQueue.setRunner(async (task) => {
|
|
2123
|
+
await runAgentLoop(task, {
|
|
2124
|
+
bus,
|
|
2125
|
+
toolRegistry,
|
|
2126
|
+
pawRegistry,
|
|
2127
|
+
skillRegistry,
|
|
2128
|
+
io,
|
|
2129
|
+
config: config.loop,
|
|
2130
|
+
toolProfiles: config.toolProfiles,
|
|
2131
|
+
rateLimiter
|
|
2132
|
+
});
|
|
2133
|
+
});
|
|
2134
|
+
const engine = {
|
|
2135
|
+
bus,
|
|
2136
|
+
toolRegistry,
|
|
2137
|
+
pawRegistry,
|
|
2138
|
+
skillRegistry,
|
|
2139
|
+
taskQueue,
|
|
2140
|
+
io,
|
|
2141
|
+
config,
|
|
2142
|
+
async start() {
|
|
2143
|
+
engineLogger.info("Starting OpenVole...");
|
|
2144
|
+
if (config.brain) {
|
|
2145
|
+
} else {
|
|
2146
|
+
engineLogger.info("No Brain Paw configured \u2014 Think step will be a no-op");
|
|
2147
|
+
}
|
|
2148
|
+
const pawConfigs = config.paws.map(normalizePawConfig);
|
|
2149
|
+
const brainConfig = pawConfigs.find((p) => p.name === config.brain);
|
|
2150
|
+
const subprocessPaws = pawConfigs.filter(
|
|
2151
|
+
(p) => p.name !== config.brain
|
|
2152
|
+
);
|
|
2153
|
+
if (brainConfig) {
|
|
2154
|
+
const ok = await pawRegistry.load(brainConfig);
|
|
2155
|
+
if (ok) {
|
|
2156
|
+
pawRegistry.setBrain(config.brain);
|
|
2157
|
+
engineLogger.info(`Brain Paw: ${pawRegistry.resolveManifestName(config.brain)}`);
|
|
2158
|
+
} else {
|
|
2159
|
+
engineLogger.error(
|
|
2160
|
+
`Brain Paw "${brainConfig.name}" failed to load \u2014 running in no-op Think mode`
|
|
2161
|
+
);
|
|
2162
|
+
pawRegistry.setBrain("");
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
for (const pawConfig of subprocessPaws) {
|
|
2166
|
+
await pawRegistry.load(pawConfig);
|
|
2167
|
+
}
|
|
2168
|
+
for (const skillName of config.skills) {
|
|
2169
|
+
await skillRegistry.load(skillName);
|
|
2170
|
+
}
|
|
2171
|
+
skillRegistry.resolve();
|
|
2172
|
+
if (config.heartbeat.enabled) {
|
|
2173
|
+
const heartbeatMdPath = path7.resolve(projectRoot, "HEARTBEAT.md");
|
|
2174
|
+
scheduler.add(
|
|
2175
|
+
"__heartbeat__",
|
|
2176
|
+
"Heartbeat wake-up",
|
|
2177
|
+
config.heartbeat.intervalMinutes,
|
|
2178
|
+
async () => {
|
|
2179
|
+
let heartbeatContent = "";
|
|
2180
|
+
try {
|
|
2181
|
+
heartbeatContent = await fs6.readFile(heartbeatMdPath, "utf-8");
|
|
2182
|
+
} catch {
|
|
2183
|
+
}
|
|
2184
|
+
const input = heartbeatContent ? `Heartbeat wake-up. Review your HEARTBEAT.md jobs and act on what is needed:
|
|
2185
|
+
|
|
2186
|
+
${heartbeatContent}` : "Heartbeat wake-up. Check active skills and decide if any actions are needed.";
|
|
2187
|
+
taskQueue.enqueue(input, "heartbeat");
|
|
2188
|
+
}
|
|
2189
|
+
);
|
|
2190
|
+
engineLogger.info(`Heartbeat enabled \u2014 interval: ${config.heartbeat.intervalMinutes}m`);
|
|
2191
|
+
}
|
|
2192
|
+
engineLogger.info(
|
|
2193
|
+
`Ready \u2014 ${toolRegistry.list().length} tools, ${pawRegistry.list().length} paws, ${skillRegistry.active().length}/${skillRegistry.list().length} skills active`
|
|
2194
|
+
);
|
|
2195
|
+
},
|
|
2196
|
+
run(input, source = "user", sessionId) {
|
|
2197
|
+
taskQueue.enqueue(input, source, sessionId ? { sessionId } : void 0);
|
|
2198
|
+
},
|
|
2199
|
+
async shutdown() {
|
|
2200
|
+
engineLogger.info("Shutting down...");
|
|
2201
|
+
scheduler.clearAll();
|
|
2202
|
+
for (const paw of pawRegistry.list()) {
|
|
2203
|
+
await pawRegistry.unload(paw.name);
|
|
2204
|
+
}
|
|
2205
|
+
toolRegistry.clear();
|
|
2206
|
+
engineLogger.info("Shutdown complete");
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
return engine;
|
|
2210
|
+
}
|
|
2211
|
+
export {
|
|
2212
|
+
PHASE_ORDER,
|
|
2213
|
+
PawRegistry,
|
|
2214
|
+
RateLimiter,
|
|
2215
|
+
SchedulerStore,
|
|
2216
|
+
SkillRegistry,
|
|
2217
|
+
TaskQueue,
|
|
2218
|
+
ToolRegistry,
|
|
2219
|
+
addPawToLock,
|
|
2220
|
+
addSkillToLock,
|
|
2221
|
+
buildActiveSkills,
|
|
2222
|
+
computeEffectivePermissions,
|
|
2223
|
+
createActionError,
|
|
2224
|
+
createAgentContext,
|
|
2225
|
+
createEngine,
|
|
2226
|
+
createMessageBus,
|
|
2227
|
+
createTtyIO,
|
|
2228
|
+
defineConfig,
|
|
2229
|
+
failureResult,
|
|
2230
|
+
loadConfig,
|
|
2231
|
+
readLockFile,
|
|
2232
|
+
readPawManifest,
|
|
2233
|
+
removePawFromLock,
|
|
2234
|
+
removeSkillFromLock,
|
|
2235
|
+
resolvePawPath,
|
|
2236
|
+
resolveSkills,
|
|
2237
|
+
runAgentLoop,
|
|
2238
|
+
successResult,
|
|
2239
|
+
validatePermissions,
|
|
2240
|
+
writeLockFile
|
|
2241
|
+
};
|
|
2242
|
+
//# sourceMappingURL=index.js.map
|