@electric-ax/agents 0.4.2 → 0.4.4

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.d.ts CHANGED
@@ -1,32 +1,10 @@
1
- import { AgentConfig, AgentTool, AvailableProvider, DispatchPolicy, EntityRegistry, EntityStreamDBWithActions, HandlerContext, HeadersProvider, PullWakeRunnerConfig, RuntimeHandler, WakeEvent } from "@electric-ax/agents-runtime";
1
+ import { AgentConfig, AgentTool, AvailableProvider, DispatchPolicy, EntityRegistry, EntityStreamDBWithActions, HandlerContext, HeadersProvider, PullWakeRunnerConfig, RuntimeHandler, SkillsRegistry, WakeEvent } from "@electric-ax/agents-runtime";
2
2
  import { ChangeEvent } from "@durable-streams/state";
3
3
  import { braveSearchTool } from "@electric-ax/agents-runtime/tools";
4
4
  import { ListedEntry as McpListedEntry, McpConfig, McpServerConfig, McpServerConfig as McpServerConfig$1, Registry, Registry as McpRegistry, RegistrySnapshot, RegistrySubscriber } from "@electric-ax/agents-mcp";
5
5
  import { AgentTool as AgentTool$1, StreamFn } from "@mariozechner/pi-agent-core";
6
6
  import { IncomingMessage, ServerResponse } from "node:http";
7
7
 
8
- //#region src/skills/types.d.ts
9
- interface SkillMeta {
10
- name: string;
11
- description: string;
12
- whenToUse: string;
13
- keywords: Array<string>;
14
- arguments?: Array<string>;
15
- argumentHint?: string;
16
- userInvocable?: boolean;
17
- max: number;
18
- charCount: number;
19
- contentHash: string;
20
- source: string;
21
- }
22
- interface SkillsRegistry {
23
- /** All skill metadata, keyed by name. */
24
- catalog: ReadonlyMap<string, SkillMeta>;
25
- /** Render the skill catalog as text for context injection. Fits within budget (chars). */
26
- renderCatalog: (budget?: number) => string;
27
- /** Read skill content from disk. Returns null if skill not found. */
28
- readContent: (name: string) => Promise<string | null>;
29
- } //#endregion
30
8
  //#region src/bootstrap.d.ts
31
9
  declare const DEFAULT_BUILTIN_AGENT_HANDLER_PATH = "/_electric/builtin-agent-handler";
32
10
  interface AgentHandlerResult {
@@ -83,9 +61,7 @@ interface BuiltinAgentHandlerOptions {
83
61
  declare function createBuiltinAgentHandler(options: BuiltinAgentHandlerOptions): Promise<AgentHandlerResult | null>;
84
62
  declare function createAgentHandler(agentServerUrl: string, workingDirectory?: string, streamFn?: StreamFn, createElectricTools?: BuiltinAgentHandlerOptions[`createElectricTools`], serveEndpoint?: string): Promise<AgentHandlerResult | null>;
85
63
  declare function registerBuiltinAgentTypes(bootstrap: AgentHandlerResult): Promise<void>;
86
- declare const registerAgentTypes: typeof registerBuiltinAgentTypes;
87
-
88
- //#endregion
64
+ declare const registerAgentTypes: typeof registerBuiltinAgentTypes; //#endregion
89
65
  //#region src/server.d.ts
90
66
  interface BuiltinAgentsServerOptions {
91
67
  agentServerUrl: string;
@@ -94,13 +70,14 @@ interface BuiltinAgentsServerOptions {
94
70
  /** Pull-wake runner configuration for built-in agents. */
95
71
  pullWake: {
96
72
  runnerId: string;
97
- ownerUserId?: string;
73
+ ownerPrincipal?: string;
98
74
  label?: string;
99
75
  registerRunner?: boolean;
100
76
  headers?: PullWakeRunnerConfig[`headers`];
101
77
  claimHeaders?: PullWakeRunnerConfig[`claimHeaders`];
102
78
  claimTokenHeader?: PullWakeRunnerConfig[`claimTokenHeader`];
103
79
  heartbeatIntervalMs?: PullWakeRunnerConfig[`heartbeatIntervalMs`];
80
+ eventHeartbeatThrottleMs?: PullWakeRunnerConfig[`eventHeartbeatThrottleMs`];
104
81
  leaseMs?: PullWakeRunnerConfig[`leaseMs`];
105
82
  };
106
83
  /** Invoked when an `authorizationCode` server needs user consent. */
@@ -177,9 +154,7 @@ declare class BuiltinAgentsServer {
177
154
  start(): Promise<string>;
178
155
  stop(): Promise<void>;
179
156
  private registerPullWakeRunner;
180
- }
181
-
182
- //#endregion
157
+ } //#endregion
183
158
  //#region src/entrypoint-lib.d.ts
184
159
  type EnvSource = Record<string, string | undefined>;
185
160
  interface BuiltinAgentsEntrypointOptions extends BuiltinAgentsServerOptions {}
@@ -260,7 +235,7 @@ declare function registerWorker(registry: EntityRegistry, options: {
260
235
 
261
236
  //#endregion
262
237
  //#region src/tools/spawn-worker.d.ts
263
- declare const WORKER_TOOL_NAMES: readonly ["bash", "read", "write", "edit", "web_search", "fetch_url", "spawn_worker"];
238
+ declare const WORKER_TOOL_NAMES: readonly ["bash", "read", "write", "edit", "web_search", "fetch_url", "spawn_worker", "send"];
264
239
  type WorkerToolName = (typeof WORKER_TOOL_NAMES)[number];
265
240
  declare function createSpawnWorkerTool(ctx: HandlerContext, modelConfig?: BuiltinAgentModelConfig): AgentTool$1;
266
241
 
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`;
@@ -1945,7 +1520,7 @@ async function createBuiltinAgentHandler(options) {
1945
1520
  subscriptionPathForType: (name) => `/${name}/*/main`,
1946
1521
  defaultDispatchPolicyForType,
1947
1522
  serverHeaders,
1948
- idleTimeout: 5e3,
1523
+ idleTimeout: 5 * 6e4,
1949
1524
  createElectricTools,
1950
1525
  publicUrl,
1951
1526
  name: runtimeName ?? `builtin-agents`
@@ -1975,19 +1550,6 @@ const registerAgentTypes = registerBuiltinAgentTypes;
1975
1550
 
1976
1551
  //#endregion
1977
1552
  //#region src/server.ts
1978
- const PRINCIPAL_KEY_PREFIXES = new Set([
1979
- `user`,
1980
- `agent`,
1981
- `service`,
1982
- `system`
1983
- ]);
1984
- function normalizeOwnerUserId(ownerUserId) {
1985
- const trimmed = ownerUserId?.trim();
1986
- if (!trimmed) return void 0;
1987
- const colon = trimmed.indexOf(`:`);
1988
- if (colon > 0 && PRINCIPAL_KEY_PREFIXES.has(trimmed.slice(0, colon))) return trimmed;
1989
- return `user:${trimmed}`;
1990
- }
1991
1553
  var BuiltinAgentsServer = class {
1992
1554
  bootstrap = null;
1993
1555
  _mcpRegistry = null;
@@ -2138,11 +1700,11 @@ var BuiltinAgentsServer = class {
2138
1700
  claimHeaders: pullWake.claimHeaders,
2139
1701
  claimTokenHeader: pullWake.claimTokenHeader,
2140
1702
  heartbeatIntervalMs: pullWake.heartbeatIntervalMs,
1703
+ eventHeartbeatThrottleMs: pullWake.eventHeartbeatThrottleMs,
2141
1704
  leaseMs: pullWake.leaseMs,
2142
1705
  offset: registeredRunner?.wake_stream_offset,
2143
1706
  onError: (error) => {
2144
1707
  serverLog.error(`[builtin-agents] pull-wake runner failed`, error);
2145
- return true;
2146
1708
  }
2147
1709
  });
2148
1710
  this.pullWakeRunner.start();
@@ -2192,18 +1754,16 @@ var BuiltinAgentsServer = class {
2192
1754
  async registerPullWakeRunner(pullWake) {
2193
1755
  const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
2194
1756
  headers.set(`content-type`, `application/json`);
2195
- const ownerUserId = normalizeOwnerUserId(pullWake.ownerUserId);
2196
- const body = {
2197
- id: pullWake.runnerId,
2198
- label: pullWake.label ?? `Built-in agents`,
2199
- kind: `local`,
2200
- admin_status: `enabled`
2201
- };
2202
- if (ownerUserId) body.owner_user_id = ownerUserId;
2203
1757
  const response = await fetch(appendPathToUrl(this.options.agentServerUrl, `/_electric/runners`), {
2204
1758
  method: `POST`,
2205
1759
  headers,
2206
- body: JSON.stringify(body)
1760
+ body: JSON.stringify({
1761
+ id: pullWake.runnerId,
1762
+ owner_principal: pullWake.ownerPrincipal,
1763
+ label: pullWake.label ?? `Built-in agents`,
1764
+ kind: `local`,
1765
+ admin_status: `enabled`
1766
+ })
2207
1767
  });
2208
1768
  if (!response.ok) throw new Error(`Failed to register pull-wake runner ${pullWake.runnerId}: ${response.status} ${await response.text()}`);
2209
1769
  return await response.json();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electric-ax/agents",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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.1"
52
+ "@electric-ax/agents-runtime": "0.3.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/better-sqlite3": "^7.6.13",