@electric-ax/agents 0.1.0 → 0.1.3
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/dist/entrypoint.js +641 -38
- package/dist/index.cjs +645 -42
- package/dist/index.d.cts +30 -4
- package/dist/index.d.ts +30 -4
- package/dist/index.js +642 -39
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { createEntityRegistry, createRuntimeHandler } from "@electric-ax/agents-runtime";
|
|
2
1
|
import path, { dirname, relative, resolve } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { createEntityRegistry, createRuntimeHandler, db } from "@electric-ax/agents-runtime";
|
|
3
4
|
import fsSync from "node:fs";
|
|
4
5
|
import pino from "pino";
|
|
5
6
|
import Anthropic from "@anthropic-ai/sdk";
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
7
8
|
import fs, { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
9
|
import Database from "better-sqlite3";
|
|
10
10
|
import { Type } from "@sinclair/typebox";
|
|
11
11
|
import { load } from "sqlite-vec";
|
|
@@ -237,12 +237,12 @@ function createFingerprint(entries) {
|
|
|
237
237
|
}
|
|
238
238
|
return hash.digest(`hex`);
|
|
239
239
|
}
|
|
240
|
-
function getMeta(db, key) {
|
|
241
|
-
const row = db.prepare(`select value from index_meta where key = ?`).get(key);
|
|
240
|
+
function getMeta(db$1, key) {
|
|
241
|
+
const row = db$1.prepare(`select value from index_meta where key = ?`).get(key);
|
|
242
242
|
return row?.value ?? null;
|
|
243
243
|
}
|
|
244
|
-
function setMeta(db, key, value) {
|
|
245
|
-
db.prepare(`insert into index_meta(key, value) values(?, ?)
|
|
244
|
+
function setMeta(db$1, key, value) {
|
|
245
|
+
db$1.prepare(`insert into index_meta(key, value) values(?, ?)
|
|
246
246
|
on conflict(key) do update set value = excluded.value`).run(key, value);
|
|
247
247
|
}
|
|
248
248
|
function reciprocalRank(rank, k = 60) {
|
|
@@ -310,11 +310,11 @@ var DocsKnowledgeBase = class {
|
|
|
310
310
|
openDatabase() {
|
|
311
311
|
fsSync.mkdirSync(path.dirname(this.dbPath), { recursive: true });
|
|
312
312
|
try {
|
|
313
|
-
const db = new Database(this.dbPath);
|
|
314
|
-
load(db);
|
|
315
|
-
db.pragma(`journal_mode = WAL`);
|
|
316
|
-
db.pragma(`synchronous = NORMAL`);
|
|
317
|
-
return db;
|
|
313
|
+
const db$1 = new Database(this.dbPath);
|
|
314
|
+
load(db$1);
|
|
315
|
+
db$1.pragma(`journal_mode = WAL`);
|
|
316
|
+
db$1.pragma(`synchronous = NORMAL`);
|
|
317
|
+
return db$1;
|
|
318
318
|
} catch (error) {
|
|
319
319
|
const message = error instanceof Error ? error.message : String(error);
|
|
320
320
|
console.warn(`${this.logPrefix} falling back to in-memory docs index: ${message}`);
|
|
@@ -407,17 +407,17 @@ var DocsKnowledgeBase = class {
|
|
|
407
407
|
console.log(`${this.logPrefix} indexed ${stats$1.docCount} docs into ${stats$1.chunkCount} chunks (${stats$1.fingerprint.slice(0, 12)}...)`);
|
|
408
408
|
return stats$1;
|
|
409
409
|
}
|
|
410
|
-
const db = this.db;
|
|
411
|
-
const currentFingerprint = getMeta(db, DOCS_FINGERPRINT_KEY);
|
|
412
|
-
const currentVersion = getMeta(db, INDEX_VERSION_KEY);
|
|
410
|
+
const db$1 = this.db;
|
|
411
|
+
const currentFingerprint = getMeta(db$1, DOCS_FINGERPRINT_KEY);
|
|
412
|
+
const currentVersion = getMeta(db$1, INDEX_VERSION_KEY);
|
|
413
413
|
if (currentFingerprint === fingerprint && currentVersion === INDEX_VERSION && this.stats().chunkCount > 0) return this.stats();
|
|
414
|
-
const insertDoc = db.prepare(`insert into docs(path, title, content) values(?, ?, ?)`);
|
|
415
|
-
const insertChunk = db.prepare(`insert into chunks(doc_path, title, heading, chunk_index, content, embedding)
|
|
414
|
+
const insertDoc = db$1.prepare(`insert into docs(path, title, content) values(?, ?, ?)`);
|
|
415
|
+
const insertChunk = db$1.prepare(`insert into chunks(doc_path, title, heading, chunk_index, content, embedding)
|
|
416
416
|
values(?, ?, ?, ?, ?, vec_f32(?))`);
|
|
417
|
-
const insertFts = db.prepare(`insert into chunks_fts(rowid, doc_path, title, heading, content)
|
|
417
|
+
const insertFts = db$1.prepare(`insert into chunks_fts(rowid, doc_path, title, heading, content)
|
|
418
418
|
values(?, ?, ?, ?, ?)`);
|
|
419
|
-
const reset = db.transaction(() => {
|
|
420
|
-
db.exec(`
|
|
419
|
+
const reset = db$1.transaction(() => {
|
|
420
|
+
db$1.exec(`
|
|
421
421
|
delete from chunks_fts;
|
|
422
422
|
delete from chunks;
|
|
423
423
|
delete from docs;
|
|
@@ -436,8 +436,8 @@ var DocsKnowledgeBase = class {
|
|
|
436
436
|
insertFts.run(Number(result.lastInsertRowid), chunk.docPath, chunk.title, chunk.heading, chunk.content);
|
|
437
437
|
}
|
|
438
438
|
}
|
|
439
|
-
setMeta(db, DOCS_FINGERPRINT_KEY, fingerprint);
|
|
440
|
-
setMeta(db, INDEX_VERSION_KEY, INDEX_VERSION);
|
|
439
|
+
setMeta(db$1, DOCS_FINGERPRINT_KEY, fingerprint);
|
|
440
|
+
setMeta(db$1, INDEX_VERSION_KEY, INDEX_VERSION);
|
|
441
441
|
});
|
|
442
442
|
reset();
|
|
443
443
|
const stats = this.stats();
|
|
@@ -660,6 +660,184 @@ function createHortonDocsSupport(workingDirectory, opts = {}) {
|
|
|
660
660
|
};
|
|
661
661
|
}
|
|
662
662
|
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/skills/tools.ts
|
|
665
|
+
function skillContextId(name) {
|
|
666
|
+
return `skill:${name}`;
|
|
667
|
+
}
|
|
668
|
+
function createSkillTools(registry, ctx) {
|
|
669
|
+
const useSkill = {
|
|
670
|
+
name: `use_skill`,
|
|
671
|
+
label: `Use Skill`,
|
|
672
|
+
description: `Load a skill into your context. Call with a skill name to load it. Pass args if the skill accepts arguments.`,
|
|
673
|
+
parameters: Type.Object({
|
|
674
|
+
name: Type.String({ description: `Name of the skill to load` }),
|
|
675
|
+
args: Type.Optional(Type.String({ description: `Arguments to pass to the skill (space-separated, or quoted for multi-word values)` }))
|
|
676
|
+
}),
|
|
677
|
+
execute: async (_toolCallId, params) => {
|
|
678
|
+
const { name, args } = params;
|
|
679
|
+
const meta = registry.catalog.get(name);
|
|
680
|
+
if (!meta) {
|
|
681
|
+
const available = Array.from(registry.catalog.keys()).join(`, `);
|
|
682
|
+
return {
|
|
683
|
+
content: [{
|
|
684
|
+
type: `text`,
|
|
685
|
+
text: `Skill "${name}" not found. Available skills: ${available || `none`}`
|
|
686
|
+
}],
|
|
687
|
+
details: { loaded: false }
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const contextId = skillContextId(name);
|
|
691
|
+
if (ctx.getContext(contextId)) return {
|
|
692
|
+
content: [{
|
|
693
|
+
type: `text`,
|
|
694
|
+
text: `Skill "${name}" is already loaded.`
|
|
695
|
+
}],
|
|
696
|
+
details: {
|
|
697
|
+
loaded: false,
|
|
698
|
+
alreadyLoaded: true
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
let content = await registry.readContent(name);
|
|
702
|
+
if (content === null) return {
|
|
703
|
+
content: [{
|
|
704
|
+
type: `text`,
|
|
705
|
+
text: `Error: could not read skill file for "${name}".`
|
|
706
|
+
}],
|
|
707
|
+
details: { loaded: false }
|
|
708
|
+
};
|
|
709
|
+
let truncated = false;
|
|
710
|
+
if (content.length > meta.max) {
|
|
711
|
+
truncated = true;
|
|
712
|
+
content = content.slice(0, meta.max);
|
|
713
|
+
}
|
|
714
|
+
if (args) content = substituteArgs(content, args, meta.arguments);
|
|
715
|
+
ctx.insertContext(contextId, {
|
|
716
|
+
name: `skill_instructions`,
|
|
717
|
+
attrs: {
|
|
718
|
+
skill: name,
|
|
719
|
+
type: `directive`
|
|
720
|
+
},
|
|
721
|
+
content
|
|
722
|
+
});
|
|
723
|
+
const skillDir = path.join(path.dirname(meta.source), name);
|
|
724
|
+
const truncNote = truncated ? `\n\nWARNING: Content was truncated from ${meta.charCount.toLocaleString()} to ${meta.max.toLocaleString()} chars. Inform the user.` : ``;
|
|
725
|
+
const allRefFiles = listRefFiles(skillDir);
|
|
726
|
+
const mdFiles = allRefFiles.filter((f) => f.endsWith(`.md`));
|
|
727
|
+
const refContents = [];
|
|
728
|
+
for (const f of mdFiles) try {
|
|
729
|
+
const refContent = await fs.readFile(path.join(skillDir, f), `utf-8`);
|
|
730
|
+
const refId = `${skillContextId(name)}:${f}`;
|
|
731
|
+
ctx.insertContext(refId, {
|
|
732
|
+
name: `skill_reference`,
|
|
733
|
+
attrs: {
|
|
734
|
+
skill: name,
|
|
735
|
+
file: f
|
|
736
|
+
},
|
|
737
|
+
content: refContent
|
|
738
|
+
});
|
|
739
|
+
refContents.push(`--- ${f} ---\n${refContent}`);
|
|
740
|
+
} catch {}
|
|
741
|
+
const hasRefDir = allRefFiles.length > 0;
|
|
742
|
+
const dirNote = hasRefDir ? `\nSkill directory: ${skillDir}` : ``;
|
|
743
|
+
const refSection = refContents.length > 0 ? `\n\n${refContents.join(`\n\n`)}` : ``;
|
|
744
|
+
const toolResult = `SKILL ACTIVATED: "${name}". The instructions below override your default behavior. Follow them exactly. Do not read any files to find this content — it is all here.\n${dirNote}${truncNote}\n\n${content}${refSection}`;
|
|
745
|
+
return {
|
|
746
|
+
content: [{
|
|
747
|
+
type: `text`,
|
|
748
|
+
text: toolResult
|
|
749
|
+
}],
|
|
750
|
+
details: {
|
|
751
|
+
loaded: true,
|
|
752
|
+
truncated,
|
|
753
|
+
chars: content.length
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
const removeSkill = {
|
|
759
|
+
name: `remove_skill`,
|
|
760
|
+
label: `Remove Skill`,
|
|
761
|
+
description: `Unload a previously loaded skill from your context.`,
|
|
762
|
+
parameters: Type.Object({ name: Type.String({ description: `Name of the skill to remove` }) }),
|
|
763
|
+
execute: async (_toolCallId, params) => {
|
|
764
|
+
const { name } = params;
|
|
765
|
+
ctx.removeContext(skillContextId(name));
|
|
766
|
+
const meta = registry.catalog.get(name);
|
|
767
|
+
if (meta) {
|
|
768
|
+
const skillDir = path.join(path.dirname(meta.source), name);
|
|
769
|
+
for (const f of listRefFiles(skillDir)) ctx.removeContext(`${skillContextId(name)}:${f}`);
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
content: [{
|
|
773
|
+
type: `text`,
|
|
774
|
+
text: `Skill "${name}" removed from context.`
|
|
775
|
+
}],
|
|
776
|
+
details: { removed: true }
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
return [useSkill, removeSkill];
|
|
781
|
+
}
|
|
782
|
+
function parseArgs(raw) {
|
|
783
|
+
const args = [];
|
|
784
|
+
let current = ``;
|
|
785
|
+
let inQuote = false;
|
|
786
|
+
let quoteChar = ``;
|
|
787
|
+
for (const ch of raw) if (inQuote) if (ch === quoteChar) inQuote = false;
|
|
788
|
+
else current += ch;
|
|
789
|
+
else if (ch === `"` || ch === `'`) {
|
|
790
|
+
inQuote = true;
|
|
791
|
+
quoteChar = ch;
|
|
792
|
+
} else if (ch === ` ` || ch === `\t`) {
|
|
793
|
+
if (current.length > 0) {
|
|
794
|
+
args.push(current);
|
|
795
|
+
current = ``;
|
|
796
|
+
}
|
|
797
|
+
} else current += ch;
|
|
798
|
+
if (current.length > 0) args.push(current);
|
|
799
|
+
return args;
|
|
800
|
+
}
|
|
801
|
+
function substituteArgs(content, rawArgs, argNames) {
|
|
802
|
+
const parsed = parseArgs(rawArgs);
|
|
803
|
+
let result = content;
|
|
804
|
+
let matched = false;
|
|
805
|
+
if (argNames) for (let i = 0; i < argNames.length && i < parsed.length; i++) {
|
|
806
|
+
const pattern = new RegExp(`\\$${argNames[i]}\\b`, `g`);
|
|
807
|
+
if (pattern.test(result)) {
|
|
808
|
+
result = result.replace(pattern, parsed[i]);
|
|
809
|
+
matched = true;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
813
|
+
const pattern = new RegExp(`\\$${i}\\b`, `g`);
|
|
814
|
+
if (pattern.test(result)) {
|
|
815
|
+
result = result.replace(pattern, parsed[i]);
|
|
816
|
+
matched = true;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (result.includes(`$ARGUMENTS`)) {
|
|
820
|
+
result = result.replace(/\$ARGUMENTS/g, rawArgs);
|
|
821
|
+
matched = true;
|
|
822
|
+
}
|
|
823
|
+
if (!matched) result += `\n\nArguments: ${rawArgs}`;
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
826
|
+
function listRefFiles(dir, prefix = ``) {
|
|
827
|
+
try {
|
|
828
|
+
const results = [];
|
|
829
|
+
for (const entry of fsSync.readdirSync(dir)) {
|
|
830
|
+
const full = path.join(dir, entry);
|
|
831
|
+
const rel = prefix ? `${prefix}/${entry}` : entry;
|
|
832
|
+
if (fsSync.statSync(full).isDirectory()) results.push(...listRefFiles(full, rel));
|
|
833
|
+
else results.push(rel);
|
|
834
|
+
}
|
|
835
|
+
return results;
|
|
836
|
+
} catch {
|
|
837
|
+
return [];
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
663
841
|
//#endregion
|
|
664
842
|
//#region src/tools/bash.ts
|
|
665
843
|
const TIMEOUT_MS = 3e4;
|
|
@@ -1257,7 +1435,22 @@ async function generateTitle(userMessage, llmCall = defaultHaikuCall) {
|
|
|
1257
1435
|
}
|
|
1258
1436
|
function buildHortonSystemPrompt(workingDirectory, opts = {}) {
|
|
1259
1437
|
const docsTools = opts.hasDocsSupport ? `\n- search_durable_agents_docs: hybrid search over the built-in Durable Agents docs index` : ``;
|
|
1438
|
+
const skillsTools = opts.hasSkills ? `\n- use_skill: load a skill (knowledge, instructions, or a tutorial) into your context to help with the user's request\n- remove_skill: unload a skill from context when you're done with it` : ``;
|
|
1260
1439
|
const docsGuidance = opts.hasDocsSupport ? `\n- You have built-in Durable Agents docs context plus a docs search tool. Use that before broad web search when the question is about this repo, Electric Agents, or Durable Agents.\n- The docs TOC and docs search results include concrete file paths under the docs tree. Use the normal read tool with those returned paths.\n- Use repo read/bash tools for non-doc files or when you need to inspect exact implementation code in the workspace.` : ``;
|
|
1440
|
+
const skillsGuidance = opts.hasSkills ? `\n# Skills\nYou have access to skills — specialized knowledge and guided workflows you can load on demand. Your context includes a skills catalog listing what's available. When the user's request matches a skill's description or keywords, load it with use_skill.
|
|
1441
|
+
|
|
1442
|
+
Some skills are user-invocable — the user can trigger them with a slash command like \`/tutorial\`. When you see a message starting with \`/\` followed by a skill name, load that skill immediately with use_skill. Pass any text after the skill name as args.
|
|
1443
|
+
|
|
1444
|
+
## IMPORTANT: How to use a loaded skill
|
|
1445
|
+
|
|
1446
|
+
When you load a skill, it becomes your primary directive for that interaction. Follow the skill's instructions exactly:
|
|
1447
|
+
|
|
1448
|
+
1. **Read all reference files first.** The use_skill tool response lists reference files with absolute paths. Read ALL of them with your read tool before responding to the user. These files contain the actual content the skill needs — without them you're guessing.
|
|
1449
|
+
2. **Follow the skill's conversation flow.** If the skill defines steps, follow them in order. Do not improvise your own approach.
|
|
1450
|
+
3. **Adopt the skill's persona and teaching style.** The skill defines how to interact — follow it.
|
|
1451
|
+
4. **Unload when done.** Use remove_skill to free context space when the skill's workflow is complete.
|
|
1452
|
+
|
|
1453
|
+
Do NOT load a skill and then ignore its instructions. The skill is there because it contains a tested, specific workflow. Your job is to execute it faithfully.` : ``;
|
|
1261
1454
|
return `You are Horton, a friendly and capable assistant. You can chat, research the web, read and edit code, run shell commands, and dispatch subagents (workers) for isolated subtasks. Be warm and engaging in conversation; be precise and concrete when working with code.
|
|
1262
1455
|
|
|
1263
1456
|
# Tools
|
|
@@ -1268,13 +1461,13 @@ function buildHortonSystemPrompt(workingDirectory, opts = {}) {
|
|
|
1268
1461
|
- brave_search: search the web
|
|
1269
1462
|
- fetch_url: fetch and convert a URL to markdown
|
|
1270
1463
|
- spawn_worker: dispatch a subagent for an isolated task
|
|
1271
|
-
${docsTools}
|
|
1464
|
+
${docsTools}${skillsTools}
|
|
1272
1465
|
|
|
1273
1466
|
# Working with files
|
|
1274
1467
|
- Prefer edit over write when modifying existing files.
|
|
1275
1468
|
- You must read a file before you can edit it.
|
|
1276
1469
|
- Use absolute paths or paths relative to the current working directory.
|
|
1277
|
-
${docsGuidance}
|
|
1470
|
+
${docsGuidance}${skillsGuidance}
|
|
1278
1471
|
|
|
1279
1472
|
# Risky actions
|
|
1280
1473
|
Pause and confirm with the user before:
|
|
@@ -1325,10 +1518,15 @@ function extractFirstUserMessage(events) {
|
|
|
1325
1518
|
return null;
|
|
1326
1519
|
}
|
|
1327
1520
|
function createAssistantHandler(options) {
|
|
1328
|
-
const { workingDirectory, streamFn, docsSupport, docsSearchTool } = options;
|
|
1521
|
+
const { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry } = options;
|
|
1522
|
+
const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0);
|
|
1329
1523
|
return async function assistantHandler(ctx, wake) {
|
|
1330
1524
|
const readSet = new Set();
|
|
1331
|
-
const tools = [
|
|
1525
|
+
const tools = [
|
|
1526
|
+
...ctx.electricTools,
|
|
1527
|
+
...createHortonTools(workingDirectory, ctx, readSet, { docsSearchTool }),
|
|
1528
|
+
...skillsRegistry && skillsRegistry.catalog.size > 0 ? createSkillTools(skillsRegistry, ctx) : []
|
|
1529
|
+
];
|
|
1332
1530
|
if (docsSupport) ctx.useContext({
|
|
1333
1531
|
sourceBudget: 1e5,
|
|
1334
1532
|
sources: {
|
|
@@ -1342,6 +1540,25 @@ function createAssistantHandler(options) {
|
|
|
1342
1540
|
max: 6e3,
|
|
1343
1541
|
cache: `volatile`
|
|
1344
1542
|
},
|
|
1543
|
+
conversation: {
|
|
1544
|
+
content: () => ctx.timelineMessages(),
|
|
1545
|
+
cache: `volatile`
|
|
1546
|
+
},
|
|
1547
|
+
...skillsRegistry && skillsRegistry.catalog.size > 0 ? { skills_catalog: {
|
|
1548
|
+
content: () => skillsRegistry.renderCatalog(2e3),
|
|
1549
|
+
max: 2e3,
|
|
1550
|
+
cache: `stable`
|
|
1551
|
+
} } : {}
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
else if (skillsRegistry && skillsRegistry.catalog.size > 0) ctx.useContext({
|
|
1555
|
+
sourceBudget: 1e5,
|
|
1556
|
+
sources: {
|
|
1557
|
+
skills_catalog: {
|
|
1558
|
+
content: () => skillsRegistry.renderCatalog(2e3),
|
|
1559
|
+
max: 2e3,
|
|
1560
|
+
cache: `stable`
|
|
1561
|
+
},
|
|
1345
1562
|
conversation: {
|
|
1346
1563
|
content: () => ctx.timelineMessages(),
|
|
1347
1564
|
cache: `volatile`
|
|
@@ -1349,7 +1566,10 @@ function createAssistantHandler(options) {
|
|
|
1349
1566
|
}
|
|
1350
1567
|
});
|
|
1351
1568
|
ctx.useAgent({
|
|
1352
|
-
systemPrompt: buildHortonSystemPrompt(workingDirectory, {
|
|
1569
|
+
systemPrompt: buildHortonSystemPrompt(workingDirectory, {
|
|
1570
|
+
hasDocsSupport: Boolean(docsSupport),
|
|
1571
|
+
hasSkills
|
|
1572
|
+
}),
|
|
1353
1573
|
model: HORTON_MODEL,
|
|
1354
1574
|
tools,
|
|
1355
1575
|
...streamFn && { streamFn }
|
|
@@ -1375,7 +1595,7 @@ function createAssistantHandler(options) {
|
|
|
1375
1595
|
};
|
|
1376
1596
|
}
|
|
1377
1597
|
function registerHorton(registry, options) {
|
|
1378
|
-
const { workingDirectory, streamFn } = options;
|
|
1598
|
+
const { workingDirectory, streamFn, skillsRegistry = null } = options;
|
|
1379
1599
|
const docsSupport = createHortonDocsSupport(workingDirectory);
|
|
1380
1600
|
const docsSearchTool = docsSupport?.createSearchTool();
|
|
1381
1601
|
docsSupport?.ensureReady().catch((error) => {
|
|
@@ -1385,7 +1605,8 @@ function registerHorton(registry, options) {
|
|
|
1385
1605
|
workingDirectory,
|
|
1386
1606
|
streamFn,
|
|
1387
1607
|
docsSupport,
|
|
1388
|
-
docsSearchTool
|
|
1608
|
+
docsSearchTool,
|
|
1609
|
+
skillsRegistry
|
|
1389
1610
|
});
|
|
1390
1611
|
registry.define(`horton`, {
|
|
1391
1612
|
description: `Friendly capable assistant — chat, code, research, dispatch`,
|
|
@@ -1407,18 +1628,33 @@ function registerHorton(registry, options) {
|
|
|
1407
1628
|
function isWorkerToolName(value) {
|
|
1408
1629
|
return typeof value === `string` && WORKER_TOOL_NAMES.includes(value);
|
|
1409
1630
|
}
|
|
1631
|
+
function isRecord(value) {
|
|
1632
|
+
return value !== null && typeof value === `object`;
|
|
1633
|
+
}
|
|
1410
1634
|
function parseWorkerArgs(value) {
|
|
1411
1635
|
if (typeof value.systemPrompt !== `string` || value.systemPrompt.length === 0) throw new Error(`[worker] systemPrompt is required`);
|
|
1412
|
-
if (!Array.isArray(value.tools) || value.tools.length === 0) throw new Error(`[worker] tools must be a non-empty array`);
|
|
1413
1636
|
const tools = [];
|
|
1414
|
-
for (const t of value.tools) {
|
|
1637
|
+
if (Array.isArray(value.tools)) for (const t of value.tools) {
|
|
1415
1638
|
if (!isWorkerToolName(t)) throw new Error(`[worker] unknown tool name: ${JSON.stringify(t)}. Valid tools: ${WORKER_TOOL_NAMES.join(`, `)}`);
|
|
1416
1639
|
if (!tools.includes(t)) tools.push(t);
|
|
1417
1640
|
}
|
|
1418
|
-
|
|
1641
|
+
const args = {
|
|
1419
1642
|
systemPrompt: value.systemPrompt,
|
|
1420
1643
|
tools
|
|
1421
1644
|
};
|
|
1645
|
+
if (value.sharedDbToolMode === `full` || value.sharedDbToolMode === `write-only`) args.sharedDbToolMode = value.sharedDbToolMode;
|
|
1646
|
+
if (value.sharedDb !== void 0) {
|
|
1647
|
+
if (!isRecord(value.sharedDb)) throw new Error(`[worker] sharedDb must be an object`);
|
|
1648
|
+
const { id, schema } = value.sharedDb;
|
|
1649
|
+
if (typeof id !== `string` || id.length === 0) throw new Error(`[worker] sharedDb.id must be a non-empty string`);
|
|
1650
|
+
if (!isRecord(schema)) throw new Error(`[worker] sharedDb.schema must be an object`);
|
|
1651
|
+
args.sharedDb = {
|
|
1652
|
+
id,
|
|
1653
|
+
schema
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
if (tools.length === 0 && !args.sharedDb) throw new Error(`[worker] must provide tools and/or sharedDb`);
|
|
1657
|
+
return args;
|
|
1422
1658
|
}
|
|
1423
1659
|
function buildToolsForWorker(tools, workingDirectory, ctx, readSet) {
|
|
1424
1660
|
const out = [];
|
|
@@ -1451,18 +1687,115 @@ const WORKER_PROMPT_FOOTER = `
|
|
|
1451
1687
|
|
|
1452
1688
|
# Reporting back
|
|
1453
1689
|
When you finish, respond with a concise report covering what was done and any key findings. The caller will relay this to the user, so it only needs the essentials.`;
|
|
1690
|
+
function buildSharedStateTools(shared, schema, mode) {
|
|
1691
|
+
const tools = [];
|
|
1692
|
+
for (const [collectionName] of Object.entries(schema)) {
|
|
1693
|
+
if (collectionName === `id`) continue;
|
|
1694
|
+
const handle = shared[collectionName];
|
|
1695
|
+
if (!handle) continue;
|
|
1696
|
+
tools.push({
|
|
1697
|
+
name: `write_${collectionName}`,
|
|
1698
|
+
label: `Write ${collectionName}`,
|
|
1699
|
+
description: `Write an entry to the shared ${collectionName} collection. The data must include a unique 'key' field.`,
|
|
1700
|
+
parameters: Type.Object({ data: Type.Record(Type.String(), Type.Unknown(), { description: `The data object to write` }) }),
|
|
1701
|
+
execute: async (_id, params) => {
|
|
1702
|
+
const { data } = params;
|
|
1703
|
+
handle.insert(data);
|
|
1704
|
+
return {
|
|
1705
|
+
content: [{
|
|
1706
|
+
type: `text`,
|
|
1707
|
+
text: `Written to ${collectionName}: ${JSON.stringify(data)}`
|
|
1708
|
+
}],
|
|
1709
|
+
details: {}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1713
|
+
if (mode === `write-only`) continue;
|
|
1714
|
+
tools.push({
|
|
1715
|
+
name: `read_${collectionName}`,
|
|
1716
|
+
label: `Read ${collectionName}`,
|
|
1717
|
+
description: `Read all entries from the shared ${collectionName} collection.`,
|
|
1718
|
+
parameters: Type.Object({}),
|
|
1719
|
+
execute: async () => {
|
|
1720
|
+
const entries = handle.toArray;
|
|
1721
|
+
return {
|
|
1722
|
+
content: [{
|
|
1723
|
+
type: `text`,
|
|
1724
|
+
text: JSON.stringify(entries, null, 2)
|
|
1725
|
+
}],
|
|
1726
|
+
details: {}
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1730
|
+
tools.push({
|
|
1731
|
+
name: `update_${collectionName}`,
|
|
1732
|
+
label: `Update ${collectionName}`,
|
|
1733
|
+
description: `Update an existing entry in the shared ${collectionName} collection by key.`,
|
|
1734
|
+
parameters: Type.Object({
|
|
1735
|
+
key: Type.String({ description: `The key of the entry to update` }),
|
|
1736
|
+
data: Type.Record(Type.String(), Type.Unknown(), { description: `The fields to update` })
|
|
1737
|
+
}),
|
|
1738
|
+
execute: async (_id, params) => {
|
|
1739
|
+
const { key, data } = params;
|
|
1740
|
+
try {
|
|
1741
|
+
handle.update(key, (draft) => {
|
|
1742
|
+
Object.assign(draft, data);
|
|
1743
|
+
});
|
|
1744
|
+
} catch (err) {
|
|
1745
|
+
return {
|
|
1746
|
+
content: [{
|
|
1747
|
+
type: `text`,
|
|
1748
|
+
text: `Failed to update ${collectionName} entry "${key}": ${err instanceof Error ? err.message : String(err)}`
|
|
1749
|
+
}],
|
|
1750
|
+
details: {}
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
return {
|
|
1754
|
+
content: [{
|
|
1755
|
+
type: `text`,
|
|
1756
|
+
text: `Updated ${collectionName} entry "${key}"`
|
|
1757
|
+
}],
|
|
1758
|
+
details: {}
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
tools.push({
|
|
1763
|
+
name: `delete_${collectionName}`,
|
|
1764
|
+
label: `Delete ${collectionName}`,
|
|
1765
|
+
description: `Delete an entry from the shared ${collectionName} collection by key.`,
|
|
1766
|
+
parameters: Type.Object({ key: Type.String({ description: `The key of the entry to delete` }) }),
|
|
1767
|
+
execute: async (_id, params) => {
|
|
1768
|
+
const { key } = params;
|
|
1769
|
+
handle.delete(key);
|
|
1770
|
+
return {
|
|
1771
|
+
content: [{
|
|
1772
|
+
type: `text`,
|
|
1773
|
+
text: `Deleted ${collectionName} entry "${key}"`
|
|
1774
|
+
}],
|
|
1775
|
+
details: {}
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
return tools;
|
|
1781
|
+
}
|
|
1454
1782
|
function registerWorker(registry, options) {
|
|
1455
1783
|
const { workingDirectory, streamFn } = options;
|
|
1456
1784
|
registry.define(`worker`, {
|
|
1457
|
-
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools).`,
|
|
1785
|
+
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
|
|
1458
1786
|
async handler(ctx) {
|
|
1459
1787
|
const args = parseWorkerArgs(ctx.args);
|
|
1460
1788
|
const readSet = new Set();
|
|
1461
|
-
const
|
|
1789
|
+
const builtinTools = buildToolsForWorker(args.tools, workingDirectory, ctx, readSet);
|
|
1790
|
+
const sharedStateTools = [];
|
|
1791
|
+
if (args.sharedDb) {
|
|
1792
|
+
const shared = await ctx.observe(db(args.sharedDb.id, args.sharedDb.schema));
|
|
1793
|
+
sharedStateTools.push(...buildSharedStateTools(shared, args.sharedDb.schema, args.sharedDbToolMode ?? `full`));
|
|
1794
|
+
}
|
|
1462
1795
|
ctx.useAgent({
|
|
1463
1796
|
systemPrompt: `${args.systemPrompt}${WORKER_PROMPT_FOOTER}`,
|
|
1464
1797
|
model: HORTON_MODEL,
|
|
1465
|
-
tools,
|
|
1798
|
+
tools: [...builtinTools, ...sharedStateTools],
|
|
1466
1799
|
...streamFn && { streamFn }
|
|
1467
1800
|
});
|
|
1468
1801
|
await ctx.agent.run();
|
|
@@ -1470,20 +1803,289 @@ function registerWorker(registry, options) {
|
|
|
1470
1803
|
});
|
|
1471
1804
|
}
|
|
1472
1805
|
|
|
1806
|
+
//#endregion
|
|
1807
|
+
//#region src/skills/preamble.ts
|
|
1808
|
+
function parsePreamble(content) {
|
|
1809
|
+
const lines = content.split(`\n`);
|
|
1810
|
+
if (lines[0]?.trim() !== `---`) return {};
|
|
1811
|
+
let closingIndex = -1;
|
|
1812
|
+
for (let i = 1; i < Math.min(lines.length, 25); i++) if (lines[i]?.trim() === `---`) {
|
|
1813
|
+
closingIndex = i;
|
|
1814
|
+
break;
|
|
1815
|
+
}
|
|
1816
|
+
if (closingIndex === -1) return {};
|
|
1817
|
+
const result = {};
|
|
1818
|
+
for (let i = 1; i < closingIndex; i++) {
|
|
1819
|
+
const line = lines[i];
|
|
1820
|
+
const colonIndex = line.indexOf(`:`);
|
|
1821
|
+
if (colonIndex === -1) continue;
|
|
1822
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1823
|
+
const rawValue = line.slice(colonIndex + 1).trim();
|
|
1824
|
+
switch (key) {
|
|
1825
|
+
case `description`:
|
|
1826
|
+
result.description = stripQuotes(rawValue);
|
|
1827
|
+
break;
|
|
1828
|
+
case `whenToUse`:
|
|
1829
|
+
result.whenToUse = stripQuotes(rawValue);
|
|
1830
|
+
break;
|
|
1831
|
+
case `keywords`: {
|
|
1832
|
+
if (rawValue.length === 0) {
|
|
1833
|
+
const items = [];
|
|
1834
|
+
for (let j = i + 1; j < closingIndex; j++) {
|
|
1835
|
+
const next = lines[j];
|
|
1836
|
+
const match = next.match(/^\s+-\s+(.+)$/);
|
|
1837
|
+
if (match) {
|
|
1838
|
+
items.push(match[1].trim());
|
|
1839
|
+
i = j;
|
|
1840
|
+
} else break;
|
|
1841
|
+
}
|
|
1842
|
+
result.keywords = items;
|
|
1843
|
+
} else result.keywords = parseKeywords(rawValue);
|
|
1844
|
+
break;
|
|
1845
|
+
}
|
|
1846
|
+
case `arguments`: {
|
|
1847
|
+
if (rawValue.length === 0) {
|
|
1848
|
+
const items = [];
|
|
1849
|
+
for (let j = i + 1; j < closingIndex; j++) {
|
|
1850
|
+
const next = lines[j];
|
|
1851
|
+
const match = next.match(/^\s+-\s+(.+)$/);
|
|
1852
|
+
if (match) {
|
|
1853
|
+
items.push(match[1].trim());
|
|
1854
|
+
i = j;
|
|
1855
|
+
} else break;
|
|
1856
|
+
}
|
|
1857
|
+
result.arguments = items;
|
|
1858
|
+
} else result.arguments = parseKeywords(rawValue);
|
|
1859
|
+
break;
|
|
1860
|
+
}
|
|
1861
|
+
case `argument-hint`:
|
|
1862
|
+
result.argumentHint = stripQuotes(rawValue);
|
|
1863
|
+
break;
|
|
1864
|
+
case `user-invocable`:
|
|
1865
|
+
result.userInvocable = rawValue === `true`;
|
|
1866
|
+
break;
|
|
1867
|
+
case `max`: {
|
|
1868
|
+
const num = parseInt(rawValue, 10);
|
|
1869
|
+
if (!Number.isNaN(num) && num > 0) result.max = num;
|
|
1870
|
+
break;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
return result;
|
|
1875
|
+
}
|
|
1876
|
+
function parseKeywords(raw) {
|
|
1877
|
+
const stripped = raw.replace(/^\[/, ``).replace(/\]$/, ``);
|
|
1878
|
+
return stripped.split(`,`).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1879
|
+
}
|
|
1880
|
+
function stripQuotes(value) {
|
|
1881
|
+
if (value.length >= 2 && value.startsWith(`"`) && value.endsWith(`"`)) return value.slice(1, -1);
|
|
1882
|
+
return value;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
//#endregion
|
|
1886
|
+
//#region src/skills/extract-meta.ts
|
|
1887
|
+
const EXTRACT_MODEL = `claude-haiku-4-5-20251001`;
|
|
1888
|
+
const DEFAULT_MAX = 1e4;
|
|
1889
|
+
async function extractSkillMeta(name, content) {
|
|
1890
|
+
const preamble = parsePreamble(content);
|
|
1891
|
+
if (preamble.description && preamble.whenToUse && preamble.keywords) return {
|
|
1892
|
+
description: preamble.description,
|
|
1893
|
+
whenToUse: preamble.whenToUse,
|
|
1894
|
+
keywords: preamble.keywords,
|
|
1895
|
+
...preamble.arguments && { arguments: preamble.arguments },
|
|
1896
|
+
...preamble.argumentHint && { argumentHint: preamble.argumentHint },
|
|
1897
|
+
...preamble.userInvocable && { userInvocable: true },
|
|
1898
|
+
max: preamble.max ?? DEFAULT_MAX
|
|
1899
|
+
};
|
|
1900
|
+
if (process.env.ANTHROPIC_API_KEY) try {
|
|
1901
|
+
return await llmExtract(name, content, preamble);
|
|
1902
|
+
} catch (err) {
|
|
1903
|
+
serverLog.warn(`[skills] LLM metadata extraction failed for "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1904
|
+
}
|
|
1905
|
+
return {
|
|
1906
|
+
description: preamble.description ?? humanize(name),
|
|
1907
|
+
whenToUse: preamble.whenToUse ?? `User asks about ${humanize(name).toLowerCase()}`,
|
|
1908
|
+
keywords: preamble.keywords ?? [name],
|
|
1909
|
+
max: preamble.max ?? DEFAULT_MAX
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
async function llmExtract(name, content, partial) {
|
|
1913
|
+
const client = new Anthropic();
|
|
1914
|
+
const truncated = content.slice(0, 8e3);
|
|
1915
|
+
const prompt = `Analyze this skill document and extract metadata. The skill is named "${name}".
|
|
1916
|
+
|
|
1917
|
+
<skill>
|
|
1918
|
+
${truncated}
|
|
1919
|
+
</skill>
|
|
1920
|
+
|
|
1921
|
+
Return ONLY a JSON object with these fields:
|
|
1922
|
+
- "description": one-line summary of what this skill provides (max 100 chars)
|
|
1923
|
+
- "whenToUse": when should an AI agent load this skill (max 200 chars)
|
|
1924
|
+
- "keywords": array of 3-8 relevant keywords
|
|
1925
|
+
|
|
1926
|
+
Return raw JSON, no markdown fences.`;
|
|
1927
|
+
const res = await client.messages.create({
|
|
1928
|
+
model: EXTRACT_MODEL,
|
|
1929
|
+
max_tokens: 256,
|
|
1930
|
+
messages: [{
|
|
1931
|
+
role: `user`,
|
|
1932
|
+
content: prompt
|
|
1933
|
+
}]
|
|
1934
|
+
});
|
|
1935
|
+
const text = res.content[0]?.type === `text` ? res.content[0].text : ``;
|
|
1936
|
+
const parsed = JSON.parse(text);
|
|
1937
|
+
return {
|
|
1938
|
+
description: partial.description ?? parsed.description ?? humanize(name),
|
|
1939
|
+
whenToUse: partial.whenToUse ?? parsed.whenToUse ?? `User asks about ${name}`,
|
|
1940
|
+
keywords: partial.keywords ?? parsed.keywords ?? [name],
|
|
1941
|
+
max: partial.max ?? DEFAULT_MAX
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
function humanize(name) {
|
|
1945
|
+
return name.replace(/[-_]/g, ` `).replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
//#endregion
|
|
1949
|
+
//#region src/skills/registry.ts
|
|
1950
|
+
const CACHE_FILENAME = `skills-cache.json`;
|
|
1951
|
+
async function createSkillsRegistry(opts) {
|
|
1952
|
+
const { baseSkillsDir, appSkillsDir, cacheDir } = opts;
|
|
1953
|
+
const cachePath = path.join(cacheDir, CACHE_FILENAME);
|
|
1954
|
+
const existingCache = await loadCache(cachePath);
|
|
1955
|
+
const files = new Map();
|
|
1956
|
+
await scanDir(baseSkillsDir, files);
|
|
1957
|
+
if (appSkillsDir) await scanDir(appSkillsDir, files);
|
|
1958
|
+
const catalog = new Map();
|
|
1959
|
+
for (const [name, filePath] of files) {
|
|
1960
|
+
const content = await fs.readFile(filePath, `utf-8`);
|
|
1961
|
+
const hash = sha256(content);
|
|
1962
|
+
const cached = existingCache[name];
|
|
1963
|
+
if (cached && cached.contentHash === hash && cached.source === filePath) {
|
|
1964
|
+
catalog.set(name, cached);
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
serverLog.info(`[skills] extracting metadata for "${name}"`);
|
|
1968
|
+
const meta = await extractSkillMeta(name, content);
|
|
1969
|
+
const entry = {
|
|
1970
|
+
name,
|
|
1971
|
+
...meta,
|
|
1972
|
+
charCount: content.length,
|
|
1973
|
+
contentHash: hash,
|
|
1974
|
+
source: filePath
|
|
1975
|
+
};
|
|
1976
|
+
catalog.set(name, entry);
|
|
1977
|
+
}
|
|
1978
|
+
await saveCache(cachePath, catalog, cacheDir);
|
|
1979
|
+
return {
|
|
1980
|
+
catalog,
|
|
1981
|
+
renderCatalog(budget) {
|
|
1982
|
+
if (catalog.size === 0) return ``;
|
|
1983
|
+
const skills = Array.from(catalog.values());
|
|
1984
|
+
const full = renderSkillList(skills, `full`);
|
|
1985
|
+
if (!budget || full.length <= budget) return full;
|
|
1986
|
+
const compact = renderSkillList(skills, `compact`);
|
|
1987
|
+
if (compact.length <= budget) return compact;
|
|
1988
|
+
return renderSkillList(skills, `names`);
|
|
1989
|
+
},
|
|
1990
|
+
async readContent(name) {
|
|
1991
|
+
const meta = catalog.get(name);
|
|
1992
|
+
if (!meta) return null;
|
|
1993
|
+
try {
|
|
1994
|
+
return await fs.readFile(meta.source, `utf-8`);
|
|
1995
|
+
} catch {
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
async function scanDir(dir, out) {
|
|
2002
|
+
let entries;
|
|
2003
|
+
try {
|
|
2004
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
2005
|
+
} catch {
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
for (const entry of entries) {
|
|
2009
|
+
if (!entry.isFile() || !entry.name.endsWith(`.md`)) continue;
|
|
2010
|
+
const name = entry.name.slice(0, -3);
|
|
2011
|
+
out.set(name, path.resolve(dir, entry.name));
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
async function loadCache(cachePath) {
|
|
2015
|
+
try {
|
|
2016
|
+
const raw = await fs.readFile(cachePath, `utf-8`);
|
|
2017
|
+
return JSON.parse(raw);
|
|
2018
|
+
} catch {
|
|
2019
|
+
return {};
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
async function saveCache(cachePath, catalog, cacheDir) {
|
|
2023
|
+
const obj = {};
|
|
2024
|
+
for (const [name, meta] of catalog) obj[name] = meta;
|
|
2025
|
+
fsSync.mkdirSync(cacheDir, { recursive: true });
|
|
2026
|
+
await fs.writeFile(cachePath, JSON.stringify(obj, null, 2), `utf-8`);
|
|
2027
|
+
}
|
|
2028
|
+
function sha256(content) {
|
|
2029
|
+
return createHash(`sha256`).update(content).digest(`hex`);
|
|
2030
|
+
}
|
|
2031
|
+
function renderSkillList(skills, mode) {
|
|
2032
|
+
const invocable = skills.filter((s) => s.userInvocable);
|
|
2033
|
+
const others = skills.filter((s) => !s.userInvocable);
|
|
2034
|
+
const lines = [`Available skills:`];
|
|
2035
|
+
if (invocable.length > 0 && mode !== `names`) {
|
|
2036
|
+
lines.push(`\nUser-invocable (the user can trigger these directly):`);
|
|
2037
|
+
for (const meta of invocable) {
|
|
2038
|
+
const hint = meta.argumentHint ? ` ${meta.argumentHint}` : ``;
|
|
2039
|
+
lines.push(`- /${meta.name}${hint} — ${mode === `compact` ? truncate(meta.description, 100) : meta.description}`);
|
|
2040
|
+
}
|
|
2041
|
+
if (others.length > 0) lines.push(``);
|
|
2042
|
+
}
|
|
2043
|
+
const all = mode === `names` ? skills : others.length > 0 ? others : invocable.length === 0 ? skills : [];
|
|
2044
|
+
for (const meta of all) {
|
|
2045
|
+
if (mode === `names`) {
|
|
2046
|
+
const prefix = meta.userInvocable ? `/${meta.name}` : meta.name;
|
|
2047
|
+
lines.push(`- ${prefix}: ${truncate(meta.description, 60)}`);
|
|
2048
|
+
continue;
|
|
2049
|
+
}
|
|
2050
|
+
lines.push(`- ${meta.name} (${meta.charCount.toLocaleString()} chars): ${mode === `compact` ? truncate(meta.description, 100) : meta.description}`);
|
|
2051
|
+
lines.push(` Use when: ${meta.whenToUse}`);
|
|
2052
|
+
if (mode === `full`) lines.push(` Keywords: ${meta.keywords.join(`, `)}`);
|
|
2053
|
+
if (meta.argumentHint) lines.push(` Usage: use_skill("${meta.name}", "${meta.argumentHint}")`);
|
|
2054
|
+
}
|
|
2055
|
+
return lines.join(`\n`);
|
|
2056
|
+
}
|
|
2057
|
+
function truncate(str, max) {
|
|
2058
|
+
return str.length <= max ? str : str.slice(0, max - 3) + `...`;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1473
2061
|
//#endregion
|
|
1474
2062
|
//#region src/bootstrap.ts
|
|
1475
2063
|
const DEFAULT_BUILTIN_AGENT_HANDLER_PATH = `/_electric/builtin-agent-handler`;
|
|
1476
|
-
function createBuiltinAgentHandler(options) {
|
|
2064
|
+
async function createBuiltinAgentHandler(options) {
|
|
1477
2065
|
const { agentServerUrl, serveEndpoint = `${agentServerUrl}${DEFAULT_BUILTIN_AGENT_HANDLER_PATH}`, workingDirectory, streamFn, createElectricTools } = options;
|
|
1478
2066
|
if (!streamFn && !process.env.ANTHROPIC_API_KEY) {
|
|
1479
2067
|
serverLog.warn(`[builtin-agents] ANTHROPIC_API_KEY not set — skipping built-in agent registration`);
|
|
1480
2068
|
return null;
|
|
1481
2069
|
}
|
|
1482
2070
|
const cwd = workingDirectory ?? process.cwd();
|
|
2071
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
2072
|
+
const baseSkillsDir = path.resolve(here, `../skills`);
|
|
2073
|
+
let skillsRegistry = null;
|
|
2074
|
+
try {
|
|
2075
|
+
skillsRegistry = await createSkillsRegistry({
|
|
2076
|
+
baseSkillsDir,
|
|
2077
|
+
appSkillsDir: path.resolve(cwd, `skills`),
|
|
2078
|
+
cacheDir: path.resolve(cwd, `.electric-agents`)
|
|
2079
|
+
});
|
|
2080
|
+
if (skillsRegistry.catalog.size > 0) serverLog.info(`[electric-agents] ${skillsRegistry.catalog.size} skill(s) loaded: ${Array.from(skillsRegistry.catalog.keys()).join(`, `)}`);
|
|
2081
|
+
} catch (err) {
|
|
2082
|
+
serverLog.warn(`[electric-agents] skills registry failed to initialize: ${err instanceof Error ? err.message : String(err)}`);
|
|
2083
|
+
}
|
|
1483
2084
|
const registry = createEntityRegistry();
|
|
1484
2085
|
const typeNames = registerHorton(registry, {
|
|
1485
2086
|
workingDirectory: cwd,
|
|
1486
|
-
streamFn
|
|
2087
|
+
streamFn,
|
|
2088
|
+
skillsRegistry
|
|
1487
2089
|
});
|
|
1488
2090
|
registerWorker(registry, {
|
|
1489
2091
|
workingDirectory: cwd,
|
|
@@ -1502,10 +2104,11 @@ function createBuiltinAgentHandler(options) {
|
|
|
1502
2104
|
handler: runtime.onEnter,
|
|
1503
2105
|
runtime,
|
|
1504
2106
|
registry,
|
|
1505
|
-
typeNames
|
|
2107
|
+
typeNames,
|
|
2108
|
+
skillsRegistry
|
|
1506
2109
|
};
|
|
1507
2110
|
}
|
|
1508
|
-
function createAgentHandler(agentServerUrl, workingDirectory, streamFn, createElectricTools, serveEndpoint) {
|
|
2111
|
+
async function createAgentHandler(agentServerUrl, workingDirectory, streamFn, createElectricTools, serveEndpoint) {
|
|
1509
2112
|
return createBuiltinAgentHandler({
|
|
1510
2113
|
agentServerUrl,
|
|
1511
2114
|
serveEndpoint,
|
|
@@ -1564,7 +2167,7 @@ var BuiltinAgentsServer = class {
|
|
|
1564
2167
|
this.publicBaseUrl = this.options.baseUrl ?? this._url;
|
|
1565
2168
|
const webhookPath = this.options.webhookPath ?? DEFAULT_BUILTIN_AGENT_HANDLER_PATH;
|
|
1566
2169
|
const serveEndpoint = new URL(webhookPath, this.publicBaseUrl.endsWith(`/`) ? this.publicBaseUrl : `${this.publicBaseUrl}/`).toString();
|
|
1567
|
-
this.bootstrap = createBuiltinAgentHandler({
|
|
2170
|
+
this.bootstrap = await createBuiltinAgentHandler({
|
|
1568
2171
|
agentServerUrl: this.options.agentServerUrl,
|
|
1569
2172
|
serveEndpoint,
|
|
1570
2173
|
workingDirectory: this.options.workingDirectory,
|