@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.cjs
CHANGED
|
@@ -22,14 +22,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
|
|
24
24
|
//#endregion
|
|
25
|
-
const __electric_ax_agents_runtime = __toESM(require("@electric-ax/agents-runtime"));
|
|
26
25
|
const node_path = __toESM(require("node:path"));
|
|
26
|
+
const node_url = __toESM(require("node:url"));
|
|
27
|
+
const __electric_ax_agents_runtime = __toESM(require("@electric-ax/agents-runtime"));
|
|
27
28
|
const node_fs = __toESM(require("node:fs"));
|
|
28
29
|
const pino = __toESM(require("pino"));
|
|
29
30
|
const __anthropic_ai_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
30
31
|
const node_crypto = __toESM(require("node:crypto"));
|
|
31
32
|
const node_fs_promises = __toESM(require("node:fs/promises"));
|
|
32
|
-
const node_url = __toESM(require("node:url"));
|
|
33
33
|
const better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
34
34
|
const __sinclair_typebox = __toESM(require("@sinclair/typebox"));
|
|
35
35
|
const sqlite_vec = __toESM(require("sqlite-vec"));
|
|
@@ -261,12 +261,12 @@ function createFingerprint(entries) {
|
|
|
261
261
|
}
|
|
262
262
|
return hash.digest(`hex`);
|
|
263
263
|
}
|
|
264
|
-
function getMeta(db, key) {
|
|
265
|
-
const row = db.prepare(`select value from index_meta where key = ?`).get(key);
|
|
264
|
+
function getMeta(db$1, key) {
|
|
265
|
+
const row = db$1.prepare(`select value from index_meta where key = ?`).get(key);
|
|
266
266
|
return row?.value ?? null;
|
|
267
267
|
}
|
|
268
|
-
function setMeta(db, key, value) {
|
|
269
|
-
db.prepare(`insert into index_meta(key, value) values(?, ?)
|
|
268
|
+
function setMeta(db$1, key, value) {
|
|
269
|
+
db$1.prepare(`insert into index_meta(key, value) values(?, ?)
|
|
270
270
|
on conflict(key) do update set value = excluded.value`).run(key, value);
|
|
271
271
|
}
|
|
272
272
|
function reciprocalRank(rank, k = 60) {
|
|
@@ -334,11 +334,11 @@ var DocsKnowledgeBase = class {
|
|
|
334
334
|
openDatabase() {
|
|
335
335
|
node_fs.default.mkdirSync(node_path.default.dirname(this.dbPath), { recursive: true });
|
|
336
336
|
try {
|
|
337
|
-
const db = new better_sqlite3.default(this.dbPath);
|
|
338
|
-
(0, sqlite_vec.load)(db);
|
|
339
|
-
db.pragma(`journal_mode = WAL`);
|
|
340
|
-
db.pragma(`synchronous = NORMAL`);
|
|
341
|
-
return db;
|
|
337
|
+
const db$1 = new better_sqlite3.default(this.dbPath);
|
|
338
|
+
(0, sqlite_vec.load)(db$1);
|
|
339
|
+
db$1.pragma(`journal_mode = WAL`);
|
|
340
|
+
db$1.pragma(`synchronous = NORMAL`);
|
|
341
|
+
return db$1;
|
|
342
342
|
} catch (error) {
|
|
343
343
|
const message = error instanceof Error ? error.message : String(error);
|
|
344
344
|
console.warn(`${this.logPrefix} falling back to in-memory docs index: ${message}`);
|
|
@@ -431,17 +431,17 @@ var DocsKnowledgeBase = class {
|
|
|
431
431
|
console.log(`${this.logPrefix} indexed ${stats$1.docCount} docs into ${stats$1.chunkCount} chunks (${stats$1.fingerprint.slice(0, 12)}...)`);
|
|
432
432
|
return stats$1;
|
|
433
433
|
}
|
|
434
|
-
const db = this.db;
|
|
435
|
-
const currentFingerprint = getMeta(db, DOCS_FINGERPRINT_KEY);
|
|
436
|
-
const currentVersion = getMeta(db, INDEX_VERSION_KEY);
|
|
434
|
+
const db$1 = this.db;
|
|
435
|
+
const currentFingerprint = getMeta(db$1, DOCS_FINGERPRINT_KEY);
|
|
436
|
+
const currentVersion = getMeta(db$1, INDEX_VERSION_KEY);
|
|
437
437
|
if (currentFingerprint === fingerprint && currentVersion === INDEX_VERSION && this.stats().chunkCount > 0) return this.stats();
|
|
438
|
-
const insertDoc = db.prepare(`insert into docs(path, title, content) values(?, ?, ?)`);
|
|
439
|
-
const insertChunk = db.prepare(`insert into chunks(doc_path, title, heading, chunk_index, content, embedding)
|
|
438
|
+
const insertDoc = db$1.prepare(`insert into docs(path, title, content) values(?, ?, ?)`);
|
|
439
|
+
const insertChunk = db$1.prepare(`insert into chunks(doc_path, title, heading, chunk_index, content, embedding)
|
|
440
440
|
values(?, ?, ?, ?, ?, vec_f32(?))`);
|
|
441
|
-
const insertFts = db.prepare(`insert into chunks_fts(rowid, doc_path, title, heading, content)
|
|
441
|
+
const insertFts = db$1.prepare(`insert into chunks_fts(rowid, doc_path, title, heading, content)
|
|
442
442
|
values(?, ?, ?, ?, ?)`);
|
|
443
|
-
const reset = db.transaction(() => {
|
|
444
|
-
db.exec(`
|
|
443
|
+
const reset = db$1.transaction(() => {
|
|
444
|
+
db$1.exec(`
|
|
445
445
|
delete from chunks_fts;
|
|
446
446
|
delete from chunks;
|
|
447
447
|
delete from docs;
|
|
@@ -460,8 +460,8 @@ var DocsKnowledgeBase = class {
|
|
|
460
460
|
insertFts.run(Number(result.lastInsertRowid), chunk.docPath, chunk.title, chunk.heading, chunk.content);
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
|
-
setMeta(db, DOCS_FINGERPRINT_KEY, fingerprint);
|
|
464
|
-
setMeta(db, INDEX_VERSION_KEY, INDEX_VERSION);
|
|
463
|
+
setMeta(db$1, DOCS_FINGERPRINT_KEY, fingerprint);
|
|
464
|
+
setMeta(db$1, INDEX_VERSION_KEY, INDEX_VERSION);
|
|
465
465
|
});
|
|
466
466
|
reset();
|
|
467
467
|
const stats = this.stats();
|
|
@@ -684,6 +684,184 @@ function createHortonDocsSupport(workingDirectory, opts = {}) {
|
|
|
684
684
|
};
|
|
685
685
|
}
|
|
686
686
|
|
|
687
|
+
//#endregion
|
|
688
|
+
//#region src/skills/tools.ts
|
|
689
|
+
function skillContextId(name) {
|
|
690
|
+
return `skill:${name}`;
|
|
691
|
+
}
|
|
692
|
+
function createSkillTools(registry, ctx) {
|
|
693
|
+
const useSkill = {
|
|
694
|
+
name: `use_skill`,
|
|
695
|
+
label: `Use Skill`,
|
|
696
|
+
description: `Load a skill into your context. Call with a skill name to load it. Pass args if the skill accepts arguments.`,
|
|
697
|
+
parameters: __sinclair_typebox.Type.Object({
|
|
698
|
+
name: __sinclair_typebox.Type.String({ description: `Name of the skill to load` }),
|
|
699
|
+
args: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String({ description: `Arguments to pass to the skill (space-separated, or quoted for multi-word values)` }))
|
|
700
|
+
}),
|
|
701
|
+
execute: async (_toolCallId, params) => {
|
|
702
|
+
const { name, args } = params;
|
|
703
|
+
const meta = registry.catalog.get(name);
|
|
704
|
+
if (!meta) {
|
|
705
|
+
const available = Array.from(registry.catalog.keys()).join(`, `);
|
|
706
|
+
return {
|
|
707
|
+
content: [{
|
|
708
|
+
type: `text`,
|
|
709
|
+
text: `Skill "${name}" not found. Available skills: ${available || `none`}`
|
|
710
|
+
}],
|
|
711
|
+
details: { loaded: false }
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const contextId = skillContextId(name);
|
|
715
|
+
if (ctx.getContext(contextId)) return {
|
|
716
|
+
content: [{
|
|
717
|
+
type: `text`,
|
|
718
|
+
text: `Skill "${name}" is already loaded.`
|
|
719
|
+
}],
|
|
720
|
+
details: {
|
|
721
|
+
loaded: false,
|
|
722
|
+
alreadyLoaded: true
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
let content = await registry.readContent(name);
|
|
726
|
+
if (content === null) return {
|
|
727
|
+
content: [{
|
|
728
|
+
type: `text`,
|
|
729
|
+
text: `Error: could not read skill file for "${name}".`
|
|
730
|
+
}],
|
|
731
|
+
details: { loaded: false }
|
|
732
|
+
};
|
|
733
|
+
let truncated = false;
|
|
734
|
+
if (content.length > meta.max) {
|
|
735
|
+
truncated = true;
|
|
736
|
+
content = content.slice(0, meta.max);
|
|
737
|
+
}
|
|
738
|
+
if (args) content = substituteArgs(content, args, meta.arguments);
|
|
739
|
+
ctx.insertContext(contextId, {
|
|
740
|
+
name: `skill_instructions`,
|
|
741
|
+
attrs: {
|
|
742
|
+
skill: name,
|
|
743
|
+
type: `directive`
|
|
744
|
+
},
|
|
745
|
+
content
|
|
746
|
+
});
|
|
747
|
+
const skillDir = node_path.default.join(node_path.default.dirname(meta.source), name);
|
|
748
|
+
const truncNote = truncated ? `\n\nWARNING: Content was truncated from ${meta.charCount.toLocaleString()} to ${meta.max.toLocaleString()} chars. Inform the user.` : ``;
|
|
749
|
+
const allRefFiles = listRefFiles(skillDir);
|
|
750
|
+
const mdFiles = allRefFiles.filter((f) => f.endsWith(`.md`));
|
|
751
|
+
const refContents = [];
|
|
752
|
+
for (const f of mdFiles) try {
|
|
753
|
+
const refContent = await node_fs_promises.default.readFile(node_path.default.join(skillDir, f), `utf-8`);
|
|
754
|
+
const refId = `${skillContextId(name)}:${f}`;
|
|
755
|
+
ctx.insertContext(refId, {
|
|
756
|
+
name: `skill_reference`,
|
|
757
|
+
attrs: {
|
|
758
|
+
skill: name,
|
|
759
|
+
file: f
|
|
760
|
+
},
|
|
761
|
+
content: refContent
|
|
762
|
+
});
|
|
763
|
+
refContents.push(`--- ${f} ---\n${refContent}`);
|
|
764
|
+
} catch {}
|
|
765
|
+
const hasRefDir = allRefFiles.length > 0;
|
|
766
|
+
const dirNote = hasRefDir ? `\nSkill directory: ${skillDir}` : ``;
|
|
767
|
+
const refSection = refContents.length > 0 ? `\n\n${refContents.join(`\n\n`)}` : ``;
|
|
768
|
+
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}`;
|
|
769
|
+
return {
|
|
770
|
+
content: [{
|
|
771
|
+
type: `text`,
|
|
772
|
+
text: toolResult
|
|
773
|
+
}],
|
|
774
|
+
details: {
|
|
775
|
+
loaded: true,
|
|
776
|
+
truncated,
|
|
777
|
+
chars: content.length
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
const removeSkill = {
|
|
783
|
+
name: `remove_skill`,
|
|
784
|
+
label: `Remove Skill`,
|
|
785
|
+
description: `Unload a previously loaded skill from your context.`,
|
|
786
|
+
parameters: __sinclair_typebox.Type.Object({ name: __sinclair_typebox.Type.String({ description: `Name of the skill to remove` }) }),
|
|
787
|
+
execute: async (_toolCallId, params) => {
|
|
788
|
+
const { name } = params;
|
|
789
|
+
ctx.removeContext(skillContextId(name));
|
|
790
|
+
const meta = registry.catalog.get(name);
|
|
791
|
+
if (meta) {
|
|
792
|
+
const skillDir = node_path.default.join(node_path.default.dirname(meta.source), name);
|
|
793
|
+
for (const f of listRefFiles(skillDir)) ctx.removeContext(`${skillContextId(name)}:${f}`);
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
content: [{
|
|
797
|
+
type: `text`,
|
|
798
|
+
text: `Skill "${name}" removed from context.`
|
|
799
|
+
}],
|
|
800
|
+
details: { removed: true }
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
return [useSkill, removeSkill];
|
|
805
|
+
}
|
|
806
|
+
function parseArgs(raw) {
|
|
807
|
+
const args = [];
|
|
808
|
+
let current = ``;
|
|
809
|
+
let inQuote = false;
|
|
810
|
+
let quoteChar = ``;
|
|
811
|
+
for (const ch of raw) if (inQuote) if (ch === quoteChar) inQuote = false;
|
|
812
|
+
else current += ch;
|
|
813
|
+
else if (ch === `"` || ch === `'`) {
|
|
814
|
+
inQuote = true;
|
|
815
|
+
quoteChar = ch;
|
|
816
|
+
} else if (ch === ` ` || ch === `\t`) {
|
|
817
|
+
if (current.length > 0) {
|
|
818
|
+
args.push(current);
|
|
819
|
+
current = ``;
|
|
820
|
+
}
|
|
821
|
+
} else current += ch;
|
|
822
|
+
if (current.length > 0) args.push(current);
|
|
823
|
+
return args;
|
|
824
|
+
}
|
|
825
|
+
function substituteArgs(content, rawArgs, argNames) {
|
|
826
|
+
const parsed = parseArgs(rawArgs);
|
|
827
|
+
let result = content;
|
|
828
|
+
let matched = false;
|
|
829
|
+
if (argNames) for (let i = 0; i < argNames.length && i < parsed.length; i++) {
|
|
830
|
+
const pattern = new RegExp(`\\$${argNames[i]}\\b`, `g`);
|
|
831
|
+
if (pattern.test(result)) {
|
|
832
|
+
result = result.replace(pattern, parsed[i]);
|
|
833
|
+
matched = true;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
837
|
+
const pattern = new RegExp(`\\$${i}\\b`, `g`);
|
|
838
|
+
if (pattern.test(result)) {
|
|
839
|
+
result = result.replace(pattern, parsed[i]);
|
|
840
|
+
matched = true;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (result.includes(`$ARGUMENTS`)) {
|
|
844
|
+
result = result.replace(/\$ARGUMENTS/g, rawArgs);
|
|
845
|
+
matched = true;
|
|
846
|
+
}
|
|
847
|
+
if (!matched) result += `\n\nArguments: ${rawArgs}`;
|
|
848
|
+
return result;
|
|
849
|
+
}
|
|
850
|
+
function listRefFiles(dir, prefix = ``) {
|
|
851
|
+
try {
|
|
852
|
+
const results = [];
|
|
853
|
+
for (const entry of node_fs.default.readdirSync(dir)) {
|
|
854
|
+
const full = node_path.default.join(dir, entry);
|
|
855
|
+
const rel = prefix ? `${prefix}/${entry}` : entry;
|
|
856
|
+
if (node_fs.default.statSync(full).isDirectory()) results.push(...listRefFiles(full, rel));
|
|
857
|
+
else results.push(rel);
|
|
858
|
+
}
|
|
859
|
+
return results;
|
|
860
|
+
} catch {
|
|
861
|
+
return [];
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
687
865
|
//#endregion
|
|
688
866
|
//#region src/tools/bash.ts
|
|
689
867
|
const TIMEOUT_MS = 3e4;
|
|
@@ -1281,7 +1459,22 @@ async function generateTitle(userMessage, llmCall = defaultHaikuCall) {
|
|
|
1281
1459
|
}
|
|
1282
1460
|
function buildHortonSystemPrompt(workingDirectory, opts = {}) {
|
|
1283
1461
|
const docsTools = opts.hasDocsSupport ? `\n- search_durable_agents_docs: hybrid search over the built-in Durable Agents docs index` : ``;
|
|
1462
|
+
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` : ``;
|
|
1284
1463
|
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.` : ``;
|
|
1464
|
+
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.
|
|
1465
|
+
|
|
1466
|
+
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.
|
|
1467
|
+
|
|
1468
|
+
## IMPORTANT: How to use a loaded skill
|
|
1469
|
+
|
|
1470
|
+
When you load a skill, it becomes your primary directive for that interaction. Follow the skill's instructions exactly:
|
|
1471
|
+
|
|
1472
|
+
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.
|
|
1473
|
+
2. **Follow the skill's conversation flow.** If the skill defines steps, follow them in order. Do not improvise your own approach.
|
|
1474
|
+
3. **Adopt the skill's persona and teaching style.** The skill defines how to interact — follow it.
|
|
1475
|
+
4. **Unload when done.** Use remove_skill to free context space when the skill's workflow is complete.
|
|
1476
|
+
|
|
1477
|
+
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.` : ``;
|
|
1285
1478
|
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.
|
|
1286
1479
|
|
|
1287
1480
|
# Tools
|
|
@@ -1292,13 +1485,13 @@ function buildHortonSystemPrompt(workingDirectory, opts = {}) {
|
|
|
1292
1485
|
- brave_search: search the web
|
|
1293
1486
|
- fetch_url: fetch and convert a URL to markdown
|
|
1294
1487
|
- spawn_worker: dispatch a subagent for an isolated task
|
|
1295
|
-
${docsTools}
|
|
1488
|
+
${docsTools}${skillsTools}
|
|
1296
1489
|
|
|
1297
1490
|
# Working with files
|
|
1298
1491
|
- Prefer edit over write when modifying existing files.
|
|
1299
1492
|
- You must read a file before you can edit it.
|
|
1300
1493
|
- Use absolute paths or paths relative to the current working directory.
|
|
1301
|
-
${docsGuidance}
|
|
1494
|
+
${docsGuidance}${skillsGuidance}
|
|
1302
1495
|
|
|
1303
1496
|
# Risky actions
|
|
1304
1497
|
Pause and confirm with the user before:
|
|
@@ -1349,10 +1542,15 @@ function extractFirstUserMessage(events) {
|
|
|
1349
1542
|
return null;
|
|
1350
1543
|
}
|
|
1351
1544
|
function createAssistantHandler(options) {
|
|
1352
|
-
const { workingDirectory, streamFn, docsSupport, docsSearchTool } = options;
|
|
1545
|
+
const { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry } = options;
|
|
1546
|
+
const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0);
|
|
1353
1547
|
return async function assistantHandler(ctx, wake) {
|
|
1354
1548
|
const readSet = new Set();
|
|
1355
|
-
const tools = [
|
|
1549
|
+
const tools = [
|
|
1550
|
+
...ctx.electricTools,
|
|
1551
|
+
...createHortonTools(workingDirectory, ctx, readSet, { docsSearchTool }),
|
|
1552
|
+
...skillsRegistry && skillsRegistry.catalog.size > 0 ? createSkillTools(skillsRegistry, ctx) : []
|
|
1553
|
+
];
|
|
1356
1554
|
if (docsSupport) ctx.useContext({
|
|
1357
1555
|
sourceBudget: 1e5,
|
|
1358
1556
|
sources: {
|
|
@@ -1366,6 +1564,25 @@ function createAssistantHandler(options) {
|
|
|
1366
1564
|
max: 6e3,
|
|
1367
1565
|
cache: `volatile`
|
|
1368
1566
|
},
|
|
1567
|
+
conversation: {
|
|
1568
|
+
content: () => ctx.timelineMessages(),
|
|
1569
|
+
cache: `volatile`
|
|
1570
|
+
},
|
|
1571
|
+
...skillsRegistry && skillsRegistry.catalog.size > 0 ? { skills_catalog: {
|
|
1572
|
+
content: () => skillsRegistry.renderCatalog(2e3),
|
|
1573
|
+
max: 2e3,
|
|
1574
|
+
cache: `stable`
|
|
1575
|
+
} } : {}
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
else if (skillsRegistry && skillsRegistry.catalog.size > 0) ctx.useContext({
|
|
1579
|
+
sourceBudget: 1e5,
|
|
1580
|
+
sources: {
|
|
1581
|
+
skills_catalog: {
|
|
1582
|
+
content: () => skillsRegistry.renderCatalog(2e3),
|
|
1583
|
+
max: 2e3,
|
|
1584
|
+
cache: `stable`
|
|
1585
|
+
},
|
|
1369
1586
|
conversation: {
|
|
1370
1587
|
content: () => ctx.timelineMessages(),
|
|
1371
1588
|
cache: `volatile`
|
|
@@ -1373,7 +1590,10 @@ function createAssistantHandler(options) {
|
|
|
1373
1590
|
}
|
|
1374
1591
|
});
|
|
1375
1592
|
ctx.useAgent({
|
|
1376
|
-
systemPrompt: buildHortonSystemPrompt(workingDirectory, {
|
|
1593
|
+
systemPrompt: buildHortonSystemPrompt(workingDirectory, {
|
|
1594
|
+
hasDocsSupport: Boolean(docsSupport),
|
|
1595
|
+
hasSkills
|
|
1596
|
+
}),
|
|
1377
1597
|
model: HORTON_MODEL,
|
|
1378
1598
|
tools,
|
|
1379
1599
|
...streamFn && { streamFn }
|
|
@@ -1399,7 +1619,7 @@ function createAssistantHandler(options) {
|
|
|
1399
1619
|
};
|
|
1400
1620
|
}
|
|
1401
1621
|
function registerHorton(registry, options) {
|
|
1402
|
-
const { workingDirectory, streamFn } = options;
|
|
1622
|
+
const { workingDirectory, streamFn, skillsRegistry = null } = options;
|
|
1403
1623
|
const docsSupport = createHortonDocsSupport(workingDirectory);
|
|
1404
1624
|
const docsSearchTool = docsSupport?.createSearchTool();
|
|
1405
1625
|
docsSupport?.ensureReady().catch((error) => {
|
|
@@ -1409,7 +1629,8 @@ function registerHorton(registry, options) {
|
|
|
1409
1629
|
workingDirectory,
|
|
1410
1630
|
streamFn,
|
|
1411
1631
|
docsSupport,
|
|
1412
|
-
docsSearchTool
|
|
1632
|
+
docsSearchTool,
|
|
1633
|
+
skillsRegistry
|
|
1413
1634
|
});
|
|
1414
1635
|
registry.define(`horton`, {
|
|
1415
1636
|
description: `Friendly capable assistant — chat, code, research, dispatch`,
|
|
@@ -1431,18 +1652,33 @@ function registerHorton(registry, options) {
|
|
|
1431
1652
|
function isWorkerToolName(value) {
|
|
1432
1653
|
return typeof value === `string` && WORKER_TOOL_NAMES.includes(value);
|
|
1433
1654
|
}
|
|
1655
|
+
function isRecord(value) {
|
|
1656
|
+
return value !== null && typeof value === `object`;
|
|
1657
|
+
}
|
|
1434
1658
|
function parseWorkerArgs(value) {
|
|
1435
1659
|
if (typeof value.systemPrompt !== `string` || value.systemPrompt.length === 0) throw new Error(`[worker] systemPrompt is required`);
|
|
1436
|
-
if (!Array.isArray(value.tools) || value.tools.length === 0) throw new Error(`[worker] tools must be a non-empty array`);
|
|
1437
1660
|
const tools = [];
|
|
1438
|
-
for (const t of value.tools) {
|
|
1661
|
+
if (Array.isArray(value.tools)) for (const t of value.tools) {
|
|
1439
1662
|
if (!isWorkerToolName(t)) throw new Error(`[worker] unknown tool name: ${JSON.stringify(t)}. Valid tools: ${WORKER_TOOL_NAMES.join(`, `)}`);
|
|
1440
1663
|
if (!tools.includes(t)) tools.push(t);
|
|
1441
1664
|
}
|
|
1442
|
-
|
|
1665
|
+
const args = {
|
|
1443
1666
|
systemPrompt: value.systemPrompt,
|
|
1444
1667
|
tools
|
|
1445
1668
|
};
|
|
1669
|
+
if (value.sharedDbToolMode === `full` || value.sharedDbToolMode === `write-only`) args.sharedDbToolMode = value.sharedDbToolMode;
|
|
1670
|
+
if (value.sharedDb !== void 0) {
|
|
1671
|
+
if (!isRecord(value.sharedDb)) throw new Error(`[worker] sharedDb must be an object`);
|
|
1672
|
+
const { id, schema } = value.sharedDb;
|
|
1673
|
+
if (typeof id !== `string` || id.length === 0) throw new Error(`[worker] sharedDb.id must be a non-empty string`);
|
|
1674
|
+
if (!isRecord(schema)) throw new Error(`[worker] sharedDb.schema must be an object`);
|
|
1675
|
+
args.sharedDb = {
|
|
1676
|
+
id,
|
|
1677
|
+
schema
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
if (tools.length === 0 && !args.sharedDb) throw new Error(`[worker] must provide tools and/or sharedDb`);
|
|
1681
|
+
return args;
|
|
1446
1682
|
}
|
|
1447
1683
|
function buildToolsForWorker(tools, workingDirectory, ctx, readSet) {
|
|
1448
1684
|
const out = [];
|
|
@@ -1475,18 +1711,115 @@ const WORKER_PROMPT_FOOTER = `
|
|
|
1475
1711
|
|
|
1476
1712
|
# Reporting back
|
|
1477
1713
|
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.`;
|
|
1714
|
+
function buildSharedStateTools(shared, schema, mode) {
|
|
1715
|
+
const tools = [];
|
|
1716
|
+
for (const [collectionName] of Object.entries(schema)) {
|
|
1717
|
+
if (collectionName === `id`) continue;
|
|
1718
|
+
const handle = shared[collectionName];
|
|
1719
|
+
if (!handle) continue;
|
|
1720
|
+
tools.push({
|
|
1721
|
+
name: `write_${collectionName}`,
|
|
1722
|
+
label: `Write ${collectionName}`,
|
|
1723
|
+
description: `Write an entry to the shared ${collectionName} collection. The data must include a unique 'key' field.`,
|
|
1724
|
+
parameters: __sinclair_typebox.Type.Object({ data: __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown(), { description: `The data object to write` }) }),
|
|
1725
|
+
execute: async (_id, params) => {
|
|
1726
|
+
const { data } = params;
|
|
1727
|
+
handle.insert(data);
|
|
1728
|
+
return {
|
|
1729
|
+
content: [{
|
|
1730
|
+
type: `text`,
|
|
1731
|
+
text: `Written to ${collectionName}: ${JSON.stringify(data)}`
|
|
1732
|
+
}],
|
|
1733
|
+
details: {}
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
if (mode === `write-only`) continue;
|
|
1738
|
+
tools.push({
|
|
1739
|
+
name: `read_${collectionName}`,
|
|
1740
|
+
label: `Read ${collectionName}`,
|
|
1741
|
+
description: `Read all entries from the shared ${collectionName} collection.`,
|
|
1742
|
+
parameters: __sinclair_typebox.Type.Object({}),
|
|
1743
|
+
execute: async () => {
|
|
1744
|
+
const entries = handle.toArray;
|
|
1745
|
+
return {
|
|
1746
|
+
content: [{
|
|
1747
|
+
type: `text`,
|
|
1748
|
+
text: JSON.stringify(entries, null, 2)
|
|
1749
|
+
}],
|
|
1750
|
+
details: {}
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
tools.push({
|
|
1755
|
+
name: `update_${collectionName}`,
|
|
1756
|
+
label: `Update ${collectionName}`,
|
|
1757
|
+
description: `Update an existing entry in the shared ${collectionName} collection by key.`,
|
|
1758
|
+
parameters: __sinclair_typebox.Type.Object({
|
|
1759
|
+
key: __sinclair_typebox.Type.String({ description: `The key of the entry to update` }),
|
|
1760
|
+
data: __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown(), { description: `The fields to update` })
|
|
1761
|
+
}),
|
|
1762
|
+
execute: async (_id, params) => {
|
|
1763
|
+
const { key, data } = params;
|
|
1764
|
+
try {
|
|
1765
|
+
handle.update(key, (draft) => {
|
|
1766
|
+
Object.assign(draft, data);
|
|
1767
|
+
});
|
|
1768
|
+
} catch (err) {
|
|
1769
|
+
return {
|
|
1770
|
+
content: [{
|
|
1771
|
+
type: `text`,
|
|
1772
|
+
text: `Failed to update ${collectionName} entry "${key}": ${err instanceof Error ? err.message : String(err)}`
|
|
1773
|
+
}],
|
|
1774
|
+
details: {}
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
return {
|
|
1778
|
+
content: [{
|
|
1779
|
+
type: `text`,
|
|
1780
|
+
text: `Updated ${collectionName} entry "${key}"`
|
|
1781
|
+
}],
|
|
1782
|
+
details: {}
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
tools.push({
|
|
1787
|
+
name: `delete_${collectionName}`,
|
|
1788
|
+
label: `Delete ${collectionName}`,
|
|
1789
|
+
description: `Delete an entry from the shared ${collectionName} collection by key.`,
|
|
1790
|
+
parameters: __sinclair_typebox.Type.Object({ key: __sinclair_typebox.Type.String({ description: `The key of the entry to delete` }) }),
|
|
1791
|
+
execute: async (_id, params) => {
|
|
1792
|
+
const { key } = params;
|
|
1793
|
+
handle.delete(key);
|
|
1794
|
+
return {
|
|
1795
|
+
content: [{
|
|
1796
|
+
type: `text`,
|
|
1797
|
+
text: `Deleted ${collectionName} entry "${key}"`
|
|
1798
|
+
}],
|
|
1799
|
+
details: {}
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
return tools;
|
|
1805
|
+
}
|
|
1478
1806
|
function registerWorker(registry, options) {
|
|
1479
1807
|
const { workingDirectory, streamFn } = options;
|
|
1480
1808
|
registry.define(`worker`, {
|
|
1481
|
-
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools).`,
|
|
1809
|
+
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
|
|
1482
1810
|
async handler(ctx) {
|
|
1483
1811
|
const args = parseWorkerArgs(ctx.args);
|
|
1484
1812
|
const readSet = new Set();
|
|
1485
|
-
const
|
|
1813
|
+
const builtinTools = buildToolsForWorker(args.tools, workingDirectory, ctx, readSet);
|
|
1814
|
+
const sharedStateTools = [];
|
|
1815
|
+
if (args.sharedDb) {
|
|
1816
|
+
const shared = await ctx.observe((0, __electric_ax_agents_runtime.db)(args.sharedDb.id, args.sharedDb.schema));
|
|
1817
|
+
sharedStateTools.push(...buildSharedStateTools(shared, args.sharedDb.schema, args.sharedDbToolMode ?? `full`));
|
|
1818
|
+
}
|
|
1486
1819
|
ctx.useAgent({
|
|
1487
1820
|
systemPrompt: `${args.systemPrompt}${WORKER_PROMPT_FOOTER}`,
|
|
1488
1821
|
model: HORTON_MODEL,
|
|
1489
|
-
tools,
|
|
1822
|
+
tools: [...builtinTools, ...sharedStateTools],
|
|
1490
1823
|
...streamFn && { streamFn }
|
|
1491
1824
|
});
|
|
1492
1825
|
await ctx.agent.run();
|
|
@@ -1494,20 +1827,289 @@ function registerWorker(registry, options) {
|
|
|
1494
1827
|
});
|
|
1495
1828
|
}
|
|
1496
1829
|
|
|
1830
|
+
//#endregion
|
|
1831
|
+
//#region src/skills/preamble.ts
|
|
1832
|
+
function parsePreamble(content) {
|
|
1833
|
+
const lines = content.split(`\n`);
|
|
1834
|
+
if (lines[0]?.trim() !== `---`) return {};
|
|
1835
|
+
let closingIndex = -1;
|
|
1836
|
+
for (let i = 1; i < Math.min(lines.length, 25); i++) if (lines[i]?.trim() === `---`) {
|
|
1837
|
+
closingIndex = i;
|
|
1838
|
+
break;
|
|
1839
|
+
}
|
|
1840
|
+
if (closingIndex === -1) return {};
|
|
1841
|
+
const result = {};
|
|
1842
|
+
for (let i = 1; i < closingIndex; i++) {
|
|
1843
|
+
const line = lines[i];
|
|
1844
|
+
const colonIndex = line.indexOf(`:`);
|
|
1845
|
+
if (colonIndex === -1) continue;
|
|
1846
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1847
|
+
const rawValue = line.slice(colonIndex + 1).trim();
|
|
1848
|
+
switch (key) {
|
|
1849
|
+
case `description`:
|
|
1850
|
+
result.description = stripQuotes(rawValue);
|
|
1851
|
+
break;
|
|
1852
|
+
case `whenToUse`:
|
|
1853
|
+
result.whenToUse = stripQuotes(rawValue);
|
|
1854
|
+
break;
|
|
1855
|
+
case `keywords`: {
|
|
1856
|
+
if (rawValue.length === 0) {
|
|
1857
|
+
const items = [];
|
|
1858
|
+
for (let j = i + 1; j < closingIndex; j++) {
|
|
1859
|
+
const next = lines[j];
|
|
1860
|
+
const match = next.match(/^\s+-\s+(.+)$/);
|
|
1861
|
+
if (match) {
|
|
1862
|
+
items.push(match[1].trim());
|
|
1863
|
+
i = j;
|
|
1864
|
+
} else break;
|
|
1865
|
+
}
|
|
1866
|
+
result.keywords = items;
|
|
1867
|
+
} else result.keywords = parseKeywords(rawValue);
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
case `arguments`: {
|
|
1871
|
+
if (rawValue.length === 0) {
|
|
1872
|
+
const items = [];
|
|
1873
|
+
for (let j = i + 1; j < closingIndex; j++) {
|
|
1874
|
+
const next = lines[j];
|
|
1875
|
+
const match = next.match(/^\s+-\s+(.+)$/);
|
|
1876
|
+
if (match) {
|
|
1877
|
+
items.push(match[1].trim());
|
|
1878
|
+
i = j;
|
|
1879
|
+
} else break;
|
|
1880
|
+
}
|
|
1881
|
+
result.arguments = items;
|
|
1882
|
+
} else result.arguments = parseKeywords(rawValue);
|
|
1883
|
+
break;
|
|
1884
|
+
}
|
|
1885
|
+
case `argument-hint`:
|
|
1886
|
+
result.argumentHint = stripQuotes(rawValue);
|
|
1887
|
+
break;
|
|
1888
|
+
case `user-invocable`:
|
|
1889
|
+
result.userInvocable = rawValue === `true`;
|
|
1890
|
+
break;
|
|
1891
|
+
case `max`: {
|
|
1892
|
+
const num = parseInt(rawValue, 10);
|
|
1893
|
+
if (!Number.isNaN(num) && num > 0) result.max = num;
|
|
1894
|
+
break;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
return result;
|
|
1899
|
+
}
|
|
1900
|
+
function parseKeywords(raw) {
|
|
1901
|
+
const stripped = raw.replace(/^\[/, ``).replace(/\]$/, ``);
|
|
1902
|
+
return stripped.split(`,`).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1903
|
+
}
|
|
1904
|
+
function stripQuotes(value) {
|
|
1905
|
+
if (value.length >= 2 && value.startsWith(`"`) && value.endsWith(`"`)) return value.slice(1, -1);
|
|
1906
|
+
return value;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
//#endregion
|
|
1910
|
+
//#region src/skills/extract-meta.ts
|
|
1911
|
+
const EXTRACT_MODEL = `claude-haiku-4-5-20251001`;
|
|
1912
|
+
const DEFAULT_MAX = 1e4;
|
|
1913
|
+
async function extractSkillMeta(name, content) {
|
|
1914
|
+
const preamble = parsePreamble(content);
|
|
1915
|
+
if (preamble.description && preamble.whenToUse && preamble.keywords) return {
|
|
1916
|
+
description: preamble.description,
|
|
1917
|
+
whenToUse: preamble.whenToUse,
|
|
1918
|
+
keywords: preamble.keywords,
|
|
1919
|
+
...preamble.arguments && { arguments: preamble.arguments },
|
|
1920
|
+
...preamble.argumentHint && { argumentHint: preamble.argumentHint },
|
|
1921
|
+
...preamble.userInvocable && { userInvocable: true },
|
|
1922
|
+
max: preamble.max ?? DEFAULT_MAX
|
|
1923
|
+
};
|
|
1924
|
+
if (process.env.ANTHROPIC_API_KEY) try {
|
|
1925
|
+
return await llmExtract(name, content, preamble);
|
|
1926
|
+
} catch (err) {
|
|
1927
|
+
serverLog.warn(`[skills] LLM metadata extraction failed for "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1928
|
+
}
|
|
1929
|
+
return {
|
|
1930
|
+
description: preamble.description ?? humanize(name),
|
|
1931
|
+
whenToUse: preamble.whenToUse ?? `User asks about ${humanize(name).toLowerCase()}`,
|
|
1932
|
+
keywords: preamble.keywords ?? [name],
|
|
1933
|
+
max: preamble.max ?? DEFAULT_MAX
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
async function llmExtract(name, content, partial) {
|
|
1937
|
+
const client = new __anthropic_ai_sdk.default();
|
|
1938
|
+
const truncated = content.slice(0, 8e3);
|
|
1939
|
+
const prompt = `Analyze this skill document and extract metadata. The skill is named "${name}".
|
|
1940
|
+
|
|
1941
|
+
<skill>
|
|
1942
|
+
${truncated}
|
|
1943
|
+
</skill>
|
|
1944
|
+
|
|
1945
|
+
Return ONLY a JSON object with these fields:
|
|
1946
|
+
- "description": one-line summary of what this skill provides (max 100 chars)
|
|
1947
|
+
- "whenToUse": when should an AI agent load this skill (max 200 chars)
|
|
1948
|
+
- "keywords": array of 3-8 relevant keywords
|
|
1949
|
+
|
|
1950
|
+
Return raw JSON, no markdown fences.`;
|
|
1951
|
+
const res = await client.messages.create({
|
|
1952
|
+
model: EXTRACT_MODEL,
|
|
1953
|
+
max_tokens: 256,
|
|
1954
|
+
messages: [{
|
|
1955
|
+
role: `user`,
|
|
1956
|
+
content: prompt
|
|
1957
|
+
}]
|
|
1958
|
+
});
|
|
1959
|
+
const text = res.content[0]?.type === `text` ? res.content[0].text : ``;
|
|
1960
|
+
const parsed = JSON.parse(text);
|
|
1961
|
+
return {
|
|
1962
|
+
description: partial.description ?? parsed.description ?? humanize(name),
|
|
1963
|
+
whenToUse: partial.whenToUse ?? parsed.whenToUse ?? `User asks about ${name}`,
|
|
1964
|
+
keywords: partial.keywords ?? parsed.keywords ?? [name],
|
|
1965
|
+
max: partial.max ?? DEFAULT_MAX
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
function humanize(name) {
|
|
1969
|
+
return name.replace(/[-_]/g, ` `).replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
//#endregion
|
|
1973
|
+
//#region src/skills/registry.ts
|
|
1974
|
+
const CACHE_FILENAME = `skills-cache.json`;
|
|
1975
|
+
async function createSkillsRegistry(opts) {
|
|
1976
|
+
const { baseSkillsDir, appSkillsDir, cacheDir } = opts;
|
|
1977
|
+
const cachePath = node_path.default.join(cacheDir, CACHE_FILENAME);
|
|
1978
|
+
const existingCache = await loadCache(cachePath);
|
|
1979
|
+
const files = new Map();
|
|
1980
|
+
await scanDir(baseSkillsDir, files);
|
|
1981
|
+
if (appSkillsDir) await scanDir(appSkillsDir, files);
|
|
1982
|
+
const catalog = new Map();
|
|
1983
|
+
for (const [name, filePath] of files) {
|
|
1984
|
+
const content = await node_fs_promises.default.readFile(filePath, `utf-8`);
|
|
1985
|
+
const hash = sha256(content);
|
|
1986
|
+
const cached = existingCache[name];
|
|
1987
|
+
if (cached && cached.contentHash === hash && cached.source === filePath) {
|
|
1988
|
+
catalog.set(name, cached);
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
serverLog.info(`[skills] extracting metadata for "${name}"`);
|
|
1992
|
+
const meta = await extractSkillMeta(name, content);
|
|
1993
|
+
const entry = {
|
|
1994
|
+
name,
|
|
1995
|
+
...meta,
|
|
1996
|
+
charCount: content.length,
|
|
1997
|
+
contentHash: hash,
|
|
1998
|
+
source: filePath
|
|
1999
|
+
};
|
|
2000
|
+
catalog.set(name, entry);
|
|
2001
|
+
}
|
|
2002
|
+
await saveCache(cachePath, catalog, cacheDir);
|
|
2003
|
+
return {
|
|
2004
|
+
catalog,
|
|
2005
|
+
renderCatalog(budget) {
|
|
2006
|
+
if (catalog.size === 0) return ``;
|
|
2007
|
+
const skills = Array.from(catalog.values());
|
|
2008
|
+
const full = renderSkillList(skills, `full`);
|
|
2009
|
+
if (!budget || full.length <= budget) return full;
|
|
2010
|
+
const compact = renderSkillList(skills, `compact`);
|
|
2011
|
+
if (compact.length <= budget) return compact;
|
|
2012
|
+
return renderSkillList(skills, `names`);
|
|
2013
|
+
},
|
|
2014
|
+
async readContent(name) {
|
|
2015
|
+
const meta = catalog.get(name);
|
|
2016
|
+
if (!meta) return null;
|
|
2017
|
+
try {
|
|
2018
|
+
return await node_fs_promises.default.readFile(meta.source, `utf-8`);
|
|
2019
|
+
} catch {
|
|
2020
|
+
return null;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
async function scanDir(dir, out) {
|
|
2026
|
+
let entries;
|
|
2027
|
+
try {
|
|
2028
|
+
entries = await node_fs_promises.default.readdir(dir, { withFileTypes: true });
|
|
2029
|
+
} catch {
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
for (const entry of entries) {
|
|
2033
|
+
if (!entry.isFile() || !entry.name.endsWith(`.md`)) continue;
|
|
2034
|
+
const name = entry.name.slice(0, -3);
|
|
2035
|
+
out.set(name, node_path.default.resolve(dir, entry.name));
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
async function loadCache(cachePath) {
|
|
2039
|
+
try {
|
|
2040
|
+
const raw = await node_fs_promises.default.readFile(cachePath, `utf-8`);
|
|
2041
|
+
return JSON.parse(raw);
|
|
2042
|
+
} catch {
|
|
2043
|
+
return {};
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
async function saveCache(cachePath, catalog, cacheDir) {
|
|
2047
|
+
const obj = {};
|
|
2048
|
+
for (const [name, meta] of catalog) obj[name] = meta;
|
|
2049
|
+
node_fs.default.mkdirSync(cacheDir, { recursive: true });
|
|
2050
|
+
await node_fs_promises.default.writeFile(cachePath, JSON.stringify(obj, null, 2), `utf-8`);
|
|
2051
|
+
}
|
|
2052
|
+
function sha256(content) {
|
|
2053
|
+
return (0, node_crypto.createHash)(`sha256`).update(content).digest(`hex`);
|
|
2054
|
+
}
|
|
2055
|
+
function renderSkillList(skills, mode) {
|
|
2056
|
+
const invocable = skills.filter((s) => s.userInvocable);
|
|
2057
|
+
const others = skills.filter((s) => !s.userInvocable);
|
|
2058
|
+
const lines = [`Available skills:`];
|
|
2059
|
+
if (invocable.length > 0 && mode !== `names`) {
|
|
2060
|
+
lines.push(`\nUser-invocable (the user can trigger these directly):`);
|
|
2061
|
+
for (const meta of invocable) {
|
|
2062
|
+
const hint = meta.argumentHint ? ` ${meta.argumentHint}` : ``;
|
|
2063
|
+
lines.push(`- /${meta.name}${hint} — ${mode === `compact` ? truncate(meta.description, 100) : meta.description}`);
|
|
2064
|
+
}
|
|
2065
|
+
if (others.length > 0) lines.push(``);
|
|
2066
|
+
}
|
|
2067
|
+
const all = mode === `names` ? skills : others.length > 0 ? others : invocable.length === 0 ? skills : [];
|
|
2068
|
+
for (const meta of all) {
|
|
2069
|
+
if (mode === `names`) {
|
|
2070
|
+
const prefix = meta.userInvocable ? `/${meta.name}` : meta.name;
|
|
2071
|
+
lines.push(`- ${prefix}: ${truncate(meta.description, 60)}`);
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
lines.push(`- ${meta.name} (${meta.charCount.toLocaleString()} chars): ${mode === `compact` ? truncate(meta.description, 100) : meta.description}`);
|
|
2075
|
+
lines.push(` Use when: ${meta.whenToUse}`);
|
|
2076
|
+
if (mode === `full`) lines.push(` Keywords: ${meta.keywords.join(`, `)}`);
|
|
2077
|
+
if (meta.argumentHint) lines.push(` Usage: use_skill("${meta.name}", "${meta.argumentHint}")`);
|
|
2078
|
+
}
|
|
2079
|
+
return lines.join(`\n`);
|
|
2080
|
+
}
|
|
2081
|
+
function truncate(str, max) {
|
|
2082
|
+
return str.length <= max ? str : str.slice(0, max - 3) + `...`;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
1497
2085
|
//#endregion
|
|
1498
2086
|
//#region src/bootstrap.ts
|
|
1499
2087
|
const DEFAULT_BUILTIN_AGENT_HANDLER_PATH = `/_electric/builtin-agent-handler`;
|
|
1500
|
-
function createBuiltinAgentHandler(options) {
|
|
2088
|
+
async function createBuiltinAgentHandler(options) {
|
|
1501
2089
|
const { agentServerUrl, serveEndpoint = `${agentServerUrl}${DEFAULT_BUILTIN_AGENT_HANDLER_PATH}`, workingDirectory, streamFn, createElectricTools } = options;
|
|
1502
2090
|
if (!streamFn && !process.env.ANTHROPIC_API_KEY) {
|
|
1503
2091
|
serverLog.warn(`[builtin-agents] ANTHROPIC_API_KEY not set — skipping built-in agent registration`);
|
|
1504
2092
|
return null;
|
|
1505
2093
|
}
|
|
1506
2094
|
const cwd = workingDirectory ?? process.cwd();
|
|
2095
|
+
const here = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
2096
|
+
const baseSkillsDir = node_path.default.resolve(here, `../skills`);
|
|
2097
|
+
let skillsRegistry = null;
|
|
2098
|
+
try {
|
|
2099
|
+
skillsRegistry = await createSkillsRegistry({
|
|
2100
|
+
baseSkillsDir,
|
|
2101
|
+
appSkillsDir: node_path.default.resolve(cwd, `skills`),
|
|
2102
|
+
cacheDir: node_path.default.resolve(cwd, `.electric-agents`)
|
|
2103
|
+
});
|
|
2104
|
+
if (skillsRegistry.catalog.size > 0) serverLog.info(`[electric-agents] ${skillsRegistry.catalog.size} skill(s) loaded: ${Array.from(skillsRegistry.catalog.keys()).join(`, `)}`);
|
|
2105
|
+
} catch (err) {
|
|
2106
|
+
serverLog.warn(`[electric-agents] skills registry failed to initialize: ${err instanceof Error ? err.message : String(err)}`);
|
|
2107
|
+
}
|
|
1507
2108
|
const registry = (0, __electric_ax_agents_runtime.createEntityRegistry)();
|
|
1508
2109
|
const typeNames = registerHorton(registry, {
|
|
1509
2110
|
workingDirectory: cwd,
|
|
1510
|
-
streamFn
|
|
2111
|
+
streamFn,
|
|
2112
|
+
skillsRegistry
|
|
1511
2113
|
});
|
|
1512
2114
|
registerWorker(registry, {
|
|
1513
2115
|
workingDirectory: cwd,
|
|
@@ -1526,10 +2128,11 @@ function createBuiltinAgentHandler(options) {
|
|
|
1526
2128
|
handler: runtime.onEnter,
|
|
1527
2129
|
runtime,
|
|
1528
2130
|
registry,
|
|
1529
|
-
typeNames
|
|
2131
|
+
typeNames,
|
|
2132
|
+
skillsRegistry
|
|
1530
2133
|
};
|
|
1531
2134
|
}
|
|
1532
|
-
function createAgentHandler(agentServerUrl, workingDirectory, streamFn, createElectricTools, serveEndpoint) {
|
|
2135
|
+
async function createAgentHandler(agentServerUrl, workingDirectory, streamFn, createElectricTools, serveEndpoint) {
|
|
1533
2136
|
return createBuiltinAgentHandler({
|
|
1534
2137
|
agentServerUrl,
|
|
1535
2138
|
serveEndpoint,
|
|
@@ -1588,7 +2191,7 @@ var BuiltinAgentsServer = class {
|
|
|
1588
2191
|
this.publicBaseUrl = this.options.baseUrl ?? this._url;
|
|
1589
2192
|
const webhookPath = this.options.webhookPath ?? DEFAULT_BUILTIN_AGENT_HANDLER_PATH;
|
|
1590
2193
|
const serveEndpoint = new URL(webhookPath, this.publicBaseUrl.endsWith(`/`) ? this.publicBaseUrl : `${this.publicBaseUrl}/`).toString();
|
|
1591
|
-
this.bootstrap = createBuiltinAgentHandler({
|
|
2194
|
+
this.bootstrap = await createBuiltinAgentHandler({
|
|
1592
2195
|
agentServerUrl: this.options.agentServerUrl,
|
|
1593
2196
|
serveEndpoint,
|
|
1594
2197
|
workingDirectory: this.options.workingDirectory,
|
|
@@ -1624,14 +2227,14 @@ var BuiltinAgentsServer = class {
|
|
|
1624
2227
|
}
|
|
1625
2228
|
async handleRequest(req, res) {
|
|
1626
2229
|
const method = req.method?.toUpperCase();
|
|
1627
|
-
const path$
|
|
2230
|
+
const path$5 = new URL(req.url ?? `/`, `http://localhost`).pathname;
|
|
1628
2231
|
const webhookPath = this.options.webhookPath ?? DEFAULT_BUILTIN_AGENT_HANDLER_PATH;
|
|
1629
|
-
if (path$
|
|
2232
|
+
if (path$5 === `/_electric/health` && method === `GET`) {
|
|
1630
2233
|
res.writeHead(200, { "content-type": `application/json` });
|
|
1631
2234
|
res.end(JSON.stringify({ status: `ok` }));
|
|
1632
2235
|
return;
|
|
1633
2236
|
}
|
|
1634
|
-
if (path$
|
|
2237
|
+
if (path$5 === webhookPath && method === `POST` && this.bootstrap) {
|
|
1635
2238
|
await this.bootstrap.handler(req, res);
|
|
1636
2239
|
return;
|
|
1637
2240
|
}
|