@electric-ax/agents 0.4.3 → 0.4.5

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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { mergeElectricPrincipalHeader } from "./server-headers-KD5yHFYT.js";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, db, detectAvailableProviders, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
4
+ import { appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillTools, createSkillsRegistry, db, detectAvailableProviders, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
5
5
  import fs from "node:fs";
6
6
  import pino from "pino";
7
7
  import { eq, not, queryOnce } from "@durable-streams/state";
@@ -13,7 +13,7 @@ import { Type } from "@sinclair/typebox";
13
13
  import { load } from "sqlite-vec";
14
14
  import { nanoid } from "nanoid";
15
15
  import { getModels } from "@mariozechner/pi-ai";
16
- import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createFetchUrlTool, createReadFileTool, createWriteTool, fetchUrlTool } from "@electric-ax/agents-runtime/tools";
16
+ import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createFetchUrlTool, createReadFileTool, createSendTool, createWriteTool, fetchUrlTool } from "@electric-ax/agents-runtime/tools";
17
17
  import { bridgeMcpTool, buildPromptTools, buildResourceTools, createRegistry, keychainPersistence, loadConfig, mcp, watchConfig } from "@electric-ax/agents-mcp";
18
18
 
19
19
  //#region src/log.ts
@@ -692,184 +692,6 @@ function createHortonDocsSupport(workingDirectory, opts = {}) {
692
692
  };
693
693
  }
694
694
 
695
- //#endregion
696
- //#region src/skills/tools.ts
697
- function skillContextId(name) {
698
- return `skill:${name}`;
699
- }
700
- function createSkillTools(registry, ctx) {
701
- const useSkill = {
702
- name: `use_skill`,
703
- label: `Use Skill`,
704
- description: `Load a skill into your context. Call with a skill name to load it. Pass args if the skill accepts arguments.`,
705
- parameters: Type.Object({
706
- name: Type.String({ description: `Name of the skill to load` }),
707
- args: Type.Optional(Type.String({ description: `Arguments to pass to the skill (space-separated, or quoted for multi-word values)` }))
708
- }),
709
- execute: async (_toolCallId, params) => {
710
- const { name, args } = params;
711
- const meta = registry.catalog.get(name);
712
- if (!meta) {
713
- const available = Array.from(registry.catalog.keys()).join(`, `);
714
- return {
715
- content: [{
716
- type: `text`,
717
- text: `Skill "${name}" not found. Available skills: ${available || `none`}`
718
- }],
719
- details: { loaded: false }
720
- };
721
- }
722
- const contextId = skillContextId(name);
723
- if (ctx.getContext(contextId)) return {
724
- content: [{
725
- type: `text`,
726
- text: `Skill "${name}" is already loaded.`
727
- }],
728
- details: {
729
- loaded: false,
730
- alreadyLoaded: true
731
- }
732
- };
733
- let content = await registry.readContent(name);
734
- if (content === null) return {
735
- content: [{
736
- type: `text`,
737
- text: `Error: could not read skill file for "${name}".`
738
- }],
739
- details: { loaded: false }
740
- };
741
- let truncated = false;
742
- if (content.length > meta.max) {
743
- truncated = true;
744
- content = content.slice(0, meta.max);
745
- }
746
- if (args) content = substituteArgs(content, args, meta.arguments);
747
- ctx.insertContext(contextId, {
748
- name: `skill_instructions`,
749
- attrs: {
750
- skill: name,
751
- type: `directive`
752
- },
753
- content
754
- });
755
- const skillDir = path.join(path.dirname(meta.source), name);
756
- const truncNote = truncated ? `\n\nWARNING: Content was truncated from ${meta.charCount.toLocaleString()} to ${meta.max.toLocaleString()} chars. Inform the user.` : ``;
757
- const allRefFiles = listRefFiles(skillDir);
758
- const mdFiles = allRefFiles.filter((f) => f.endsWith(`.md`));
759
- const refContents = [];
760
- for (const f of mdFiles) try {
761
- const refContent = await fs$1.readFile(path.join(skillDir, f), `utf-8`);
762
- const refId = `${skillContextId(name)}:${f}`;
763
- ctx.insertContext(refId, {
764
- name: `skill_reference`,
765
- attrs: {
766
- skill: name,
767
- file: f
768
- },
769
- content: refContent
770
- });
771
- refContents.push(`--- ${f} ---\n${refContent}`);
772
- } catch {}
773
- const hasRefDir = allRefFiles.length > 0;
774
- const dirNote = hasRefDir ? `\nSkill directory: ${skillDir}` : ``;
775
- const refSection = refContents.length > 0 ? `\n\n${refContents.join(`\n\n`)}` : ``;
776
- 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}`;
777
- return {
778
- content: [{
779
- type: `text`,
780
- text: toolResult
781
- }],
782
- details: {
783
- loaded: true,
784
- truncated,
785
- chars: content.length
786
- }
787
- };
788
- }
789
- };
790
- const removeSkill = {
791
- name: `remove_skill`,
792
- label: `Remove Skill`,
793
- description: `Unload a previously loaded skill from your context.`,
794
- parameters: Type.Object({ name: Type.String({ description: `Name of the skill to remove` }) }),
795
- execute: async (_toolCallId, params) => {
796
- const { name } = params;
797
- ctx.removeContext(skillContextId(name));
798
- const meta = registry.catalog.get(name);
799
- if (meta) {
800
- const skillDir = path.join(path.dirname(meta.source), name);
801
- for (const f of listRefFiles(skillDir)) ctx.removeContext(`${skillContextId(name)}:${f}`);
802
- }
803
- return {
804
- content: [{
805
- type: `text`,
806
- text: `Skill "${name}" removed from context.`
807
- }],
808
- details: { removed: true }
809
- };
810
- }
811
- };
812
- return [useSkill, removeSkill];
813
- }
814
- function parseArgs(raw) {
815
- const args = [];
816
- let current = ``;
817
- let inQuote = false;
818
- let quoteChar = ``;
819
- for (const ch of raw) if (inQuote) if (ch === quoteChar) inQuote = false;
820
- else current += ch;
821
- else if (ch === `"` || ch === `'`) {
822
- inQuote = true;
823
- quoteChar = ch;
824
- } else if (ch === ` ` || ch === `\t`) {
825
- if (current.length > 0) {
826
- args.push(current);
827
- current = ``;
828
- }
829
- } else current += ch;
830
- if (current.length > 0) args.push(current);
831
- return args;
832
- }
833
- function substituteArgs(content, rawArgs, argNames) {
834
- const parsed = parseArgs(rawArgs);
835
- let result = content;
836
- let matched = false;
837
- if (argNames) for (let i = 0; i < argNames.length && i < parsed.length; i++) {
838
- const pattern = new RegExp(`\\$${argNames[i]}\\b`, `g`);
839
- if (pattern.test(result)) {
840
- result = result.replace(pattern, parsed[i]);
841
- matched = true;
842
- }
843
- }
844
- for (let i = 0; i < parsed.length; i++) {
845
- const pattern = new RegExp(`\\$${i}\\b`, `g`);
846
- if (pattern.test(result)) {
847
- result = result.replace(pattern, parsed[i]);
848
- matched = true;
849
- }
850
- }
851
- if (result.includes(`$ARGUMENTS`)) {
852
- result = result.replace(/\$ARGUMENTS/g, rawArgs);
853
- matched = true;
854
- }
855
- if (!matched) result += `\n\nArguments: ${rawArgs}`;
856
- return result;
857
- }
858
- function listRefFiles(dir, prefix = ``) {
859
- try {
860
- const results = [];
861
- for (const entry of fs.readdirSync(dir)) {
862
- const full = path.join(dir, entry);
863
- const rel = prefix ? `${prefix}/${entry}` : entry;
864
- if (fs.statSync(full).isDirectory()) results.push(...listRefFiles(full, rel));
865
- else results.push(rel);
866
- }
867
- return results;
868
- } catch {
869
- return [];
870
- }
871
- }
872
-
873
695
  //#endregion
874
696
  //#region src/tools/spawn-worker.ts
875
697
  const WORKER_TOOL_NAMES = [
@@ -879,7 +701,8 @@ const WORKER_TOOL_NAMES = [
879
701
  `edit`,
880
702
  `web_search`,
881
703
  `fetch_url`,
882
- `spawn_worker`
704
+ `spawn_worker`,
705
+ `send`
883
706
  ];
884
707
  function createSpawnWorkerTool(ctx, modelConfig) {
885
708
  return {
@@ -1229,6 +1052,7 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
1229
1052
  - web_search: search the web
1230
1053
  - fetch_url: fetch and convert a URL to markdown
1231
1054
  - spawn_worker: dispatch a subagent for an isolated task
1055
+ - send: send a message to an Electric Agent/entity by entity URL
1232
1056
  ${docsTools}${skillsTools}
1233
1057
 
1234
1058
  # Working with files
@@ -1276,6 +1100,7 @@ function createHortonTools(workingDirectory, ctx, readSet, opts = {}) {
1276
1100
  logPrefix: opts.logPrefix ?? `[horton]`
1277
1101
  })] : [fetchUrlTool],
1278
1102
  createSpawnWorkerTool(ctx, opts.modelConfig),
1103
+ createSendTool(ctx.send),
1279
1104
  ...opts.docsSearchTool ? [opts.docsSearchTool] : []
1280
1105
  ];
1281
1106
  }
@@ -1524,6 +1349,9 @@ function buildToolsForWorker(tools, workingDirectory, ctx, readSet) {
1524
1349
  case `spawn_worker`:
1525
1350
  out.push(createSpawnWorkerTool(ctx));
1526
1351
  break;
1352
+ case `send`:
1353
+ out.push(createSendTool(ctx.send));
1354
+ break;
1527
1355
  }
1528
1356
  return out;
1529
1357
  }
@@ -1648,259 +1476,6 @@ function registerWorker(registry, options) {
1648
1476
  });
1649
1477
  }
1650
1478
 
1651
- //#endregion
1652
- //#region src/skills/preamble.ts
1653
- function parsePreamble(content) {
1654
- const lines = content.split(`\n`);
1655
- if (lines[0]?.trim() !== `---`) return {};
1656
- let closingIndex = -1;
1657
- for (let i = 1; i < Math.min(lines.length, 25); i++) if (lines[i]?.trim() === `---`) {
1658
- closingIndex = i;
1659
- break;
1660
- }
1661
- if (closingIndex === -1) return {};
1662
- const result = {};
1663
- for (let i = 1; i < closingIndex; i++) {
1664
- const line = lines[i];
1665
- const colonIndex = line.indexOf(`:`);
1666
- if (colonIndex === -1) continue;
1667
- const key = line.slice(0, colonIndex).trim();
1668
- const rawValue = line.slice(colonIndex + 1).trim();
1669
- switch (key) {
1670
- case `description`:
1671
- result.description = stripQuotes(rawValue);
1672
- break;
1673
- case `whenToUse`:
1674
- result.whenToUse = stripQuotes(rawValue);
1675
- break;
1676
- case `keywords`: {
1677
- if (rawValue.length === 0) {
1678
- const items = [];
1679
- for (let j = i + 1; j < closingIndex; j++) {
1680
- const next = lines[j];
1681
- const match = next.match(/^\s+-\s+(.+)$/);
1682
- if (match) {
1683
- items.push(match[1].trim());
1684
- i = j;
1685
- } else break;
1686
- }
1687
- result.keywords = items;
1688
- } else result.keywords = parseKeywords(rawValue);
1689
- break;
1690
- }
1691
- case `arguments`: {
1692
- if (rawValue.length === 0) {
1693
- const items = [];
1694
- for (let j = i + 1; j < closingIndex; j++) {
1695
- const next = lines[j];
1696
- const match = next.match(/^\s+-\s+(.+)$/);
1697
- if (match) {
1698
- items.push(match[1].trim());
1699
- i = j;
1700
- } else break;
1701
- }
1702
- result.arguments = items;
1703
- } else result.arguments = parseKeywords(rawValue);
1704
- break;
1705
- }
1706
- case `argument-hint`:
1707
- result.argumentHint = stripQuotes(rawValue);
1708
- break;
1709
- case `user-invocable`:
1710
- result.userInvocable = rawValue === `true`;
1711
- break;
1712
- case `max`: {
1713
- const num = parseInt(rawValue, 10);
1714
- if (!Number.isNaN(num) && num > 0) result.max = num;
1715
- break;
1716
- }
1717
- }
1718
- }
1719
- return result;
1720
- }
1721
- function parseKeywords(raw) {
1722
- const stripped = raw.replace(/^\[/, ``).replace(/\]$/, ``);
1723
- return stripped.split(`,`).map((s) => s.trim()).filter((s) => s.length > 0);
1724
- }
1725
- function stripQuotes(value) {
1726
- if (value.length >= 2 && value.startsWith(`"`) && value.endsWith(`"`)) return value.slice(1, -1);
1727
- return value;
1728
- }
1729
-
1730
- //#endregion
1731
- //#region src/skills/extract-meta.ts
1732
- const DEFAULT_MAX = 1e4;
1733
- async function extractSkillMeta(name, content) {
1734
- const preamble = parsePreamble(content);
1735
- if (preamble.description && preamble.whenToUse && preamble.keywords) return {
1736
- description: preamble.description,
1737
- whenToUse: preamble.whenToUse,
1738
- keywords: preamble.keywords,
1739
- ...preamble.arguments && { arguments: preamble.arguments },
1740
- ...preamble.argumentHint && { argumentHint: preamble.argumentHint },
1741
- ...preamble.userInvocable && { userInvocable: true },
1742
- max: preamble.max ?? DEFAULT_MAX
1743
- };
1744
- try {
1745
- return await llmExtract(name, content, preamble);
1746
- } catch (err) {
1747
- serverLog.warn(`[skills] LLM metadata extraction failed for "${name}": ${err instanceof Error ? err.message : String(err)}`);
1748
- }
1749
- return {
1750
- description: preamble.description ?? humanize(name),
1751
- whenToUse: preamble.whenToUse ?? `User asks about ${humanize(name).toLowerCase()}`,
1752
- keywords: preamble.keywords ?? [name],
1753
- max: preamble.max ?? DEFAULT_MAX
1754
- };
1755
- }
1756
- async function llmExtract(name, content, partial) {
1757
- const truncated = content.slice(0, 8e3);
1758
- const prompt = `Analyze this skill document and extract metadata. The skill is named "${name}".
1759
-
1760
- <skill>
1761
- ${truncated}
1762
- </skill>
1763
-
1764
- Return ONLY a JSON object with these fields:
1765
- - "description": one-line summary of what this skill provides (max 100 chars)
1766
- - "whenToUse": when should an AI agent load this skill (max 200 chars)
1767
- - "keywords": array of 3-8 relevant keywords
1768
-
1769
- Return raw JSON, no markdown fences.`;
1770
- const text = await completeWithLowCostModel({
1771
- purpose: `skill metadata extraction`,
1772
- systemPrompt: `Extract metadata from skill documents. Return only valid JSON that matches the requested schema.`,
1773
- prompt,
1774
- maxTokens: 256,
1775
- log: (message) => serverLog.info(message),
1776
- logPrefix: `[skills]`
1777
- });
1778
- const parsed = JSON.parse(text);
1779
- return {
1780
- description: partial.description ?? parsed.description ?? humanize(name),
1781
- whenToUse: partial.whenToUse ?? parsed.whenToUse ?? `User asks about ${name}`,
1782
- keywords: partial.keywords ?? parsed.keywords ?? [name],
1783
- max: partial.max ?? DEFAULT_MAX
1784
- };
1785
- }
1786
- function humanize(name) {
1787
- return name.replace(/[-_]/g, ` `).replace(/\b\w/g, (c) => c.toUpperCase());
1788
- }
1789
-
1790
- //#endregion
1791
- //#region src/skills/registry.ts
1792
- const CACHE_FILENAME = `skills-cache.json`;
1793
- async function createSkillsRegistry(opts) {
1794
- const { baseSkillsDir, appSkillsDir, cacheDir } = opts;
1795
- const cachePath = path.join(cacheDir, CACHE_FILENAME);
1796
- const existingCache = await loadCache(cachePath);
1797
- const files = new Map();
1798
- await scanDir(baseSkillsDir, files);
1799
- if (appSkillsDir) await scanDir(appSkillsDir, files);
1800
- const catalog = new Map();
1801
- for (const [name, filePath] of files) {
1802
- const content = await fs$1.readFile(filePath, `utf-8`);
1803
- const hash = sha256(content);
1804
- const cached = existingCache[name];
1805
- if (cached && cached.contentHash === hash && cached.source === filePath) {
1806
- catalog.set(name, cached);
1807
- continue;
1808
- }
1809
- serverLog.info(`[skills] extracting metadata for "${name}"`);
1810
- const meta = await extractSkillMeta(name, content);
1811
- const entry = {
1812
- name,
1813
- ...meta,
1814
- charCount: content.length,
1815
- contentHash: hash,
1816
- source: filePath
1817
- };
1818
- catalog.set(name, entry);
1819
- }
1820
- await saveCache(cachePath, catalog, cacheDir);
1821
- return {
1822
- catalog,
1823
- renderCatalog(budget) {
1824
- if (catalog.size === 0) return ``;
1825
- const skills = Array.from(catalog.values());
1826
- const full = renderSkillList(skills, `full`);
1827
- if (!budget || full.length <= budget) return full;
1828
- const compact = renderSkillList(skills, `compact`);
1829
- if (compact.length <= budget) return compact;
1830
- return renderSkillList(skills, `names`);
1831
- },
1832
- async readContent(name) {
1833
- const meta = catalog.get(name);
1834
- if (!meta) return null;
1835
- try {
1836
- return await fs$1.readFile(meta.source, `utf-8`);
1837
- } catch {
1838
- return null;
1839
- }
1840
- }
1841
- };
1842
- }
1843
- async function scanDir(dir, out) {
1844
- let entries;
1845
- try {
1846
- entries = await fs$1.readdir(dir, { withFileTypes: true });
1847
- } catch {
1848
- return;
1849
- }
1850
- for (const entry of entries) {
1851
- if (!entry.isFile() || !entry.name.endsWith(`.md`)) continue;
1852
- const name = entry.name.slice(0, -3);
1853
- out.set(name, path.resolve(dir, entry.name));
1854
- }
1855
- }
1856
- async function loadCache(cachePath) {
1857
- try {
1858
- const raw = await fs$1.readFile(cachePath, `utf-8`);
1859
- return JSON.parse(raw);
1860
- } catch {
1861
- return {};
1862
- }
1863
- }
1864
- async function saveCache(cachePath, catalog, cacheDir) {
1865
- const obj = {};
1866
- for (const [name, meta] of catalog) obj[name] = meta;
1867
- fs.mkdirSync(cacheDir, { recursive: true });
1868
- await fs$1.writeFile(path.join(cacheDir, `.gitignore`), `*\n`, `utf-8`);
1869
- await fs$1.writeFile(cachePath, JSON.stringify(obj, null, 2), `utf-8`);
1870
- }
1871
- function sha256(content) {
1872
- return createHash(`sha256`).update(content).digest(`hex`);
1873
- }
1874
- function renderSkillList(skills, mode) {
1875
- const invocable = skills.filter((s) => s.userInvocable);
1876
- const others = skills.filter((s) => !s.userInvocable);
1877
- const lines = [`Available skills:`];
1878
- if (invocable.length > 0 && mode !== `names`) {
1879
- lines.push(`\nUser-invocable (the user can trigger these directly):`);
1880
- for (const meta of invocable) {
1881
- const hint = meta.argumentHint ? ` ${meta.argumentHint}` : ``;
1882
- lines.push(`- /${meta.name}${hint} — ${mode === `compact` ? truncate(meta.description, 100) : meta.description}`);
1883
- }
1884
- if (others.length > 0) lines.push(``);
1885
- }
1886
- const all = mode === `names` ? skills : others.length > 0 ? others : invocable.length === 0 ? skills : [];
1887
- for (const meta of all) {
1888
- if (mode === `names`) {
1889
- const prefix = meta.userInvocable ? `/${meta.name}` : meta.name;
1890
- lines.push(`- ${prefix}: ${truncate(meta.description, 60)}`);
1891
- continue;
1892
- }
1893
- lines.push(`- ${meta.name} (${meta.charCount.toLocaleString()} chars): ${mode === `compact` ? truncate(meta.description, 100) : meta.description}`);
1894
- lines.push(` Use when: ${meta.whenToUse}`);
1895
- if (mode === `full`) lines.push(` Keywords: ${meta.keywords.join(`, `)}`);
1896
- if (meta.argumentHint) lines.push(` Usage: use_skill("${meta.name}", "${meta.argumentHint}")`);
1897
- }
1898
- return lines.join(`\n`);
1899
- }
1900
- function truncate(str, max) {
1901
- return str.length <= max ? str : str.slice(0, max - 3) + `...`;
1902
- }
1903
-
1904
1479
  //#endregion
1905
1480
  //#region src/bootstrap.ts
1906
1481
  const DEFAULT_BUILTIN_AGENT_HANDLER_PATH = `/_electric/builtin-agent-handler`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electric-ax/agents",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Built-in Electric Agents runtimes such as Horton and worker",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,7 +38,7 @@
38
38
  "./package.json": "./package.json"
39
39
  },
40
40
  "dependencies": {
41
- "@durable-streams/state": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/state@350",
41
+ "@durable-streams/state": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/state@5d5c217",
42
42
  "@mariozechner/pi-agent-core": "^0.70.2",
43
43
  "@mariozechner/pi-ai": "^0.70.2",
44
44
  "@sinclair/typebox": "^0.34.48",
@@ -49,7 +49,7 @@
49
49
  "sqlite-vec": "^0.1.9",
50
50
  "zod": "^4.3.6",
51
51
  "@electric-ax/agents-mcp": "0.2.2",
52
- "@electric-ax/agents-runtime": "0.2.2"
52
+ "@electric-ax/agents-runtime": "0.3.1"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/better-sqlite3": "^7.6.13",