@electric-ax/agents 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1673 @@
1
+ import { createEntityRegistry, createRuntimeHandler } from "@electric-ax/agents-runtime";
2
+ import path, { dirname, relative, resolve } from "node:path";
3
+ import fsSync from "node:fs";
4
+ import pino from "pino";
5
+ import Anthropic from "@anthropic-ai/sdk";
6
+ import { createHash } from "node:crypto";
7
+ import fs, { mkdir, readFile, stat, writeFile } from "node:fs/promises";
8
+ import { fileURLToPath } from "node:url";
9
+ import Database from "better-sqlite3";
10
+ import { Type } from "@sinclair/typebox";
11
+ import { load } from "sqlite-vec";
12
+ import { exec } from "node:child_process";
13
+ import { createRequire } from "node:module";
14
+ import { Readability } from "@mozilla/readability";
15
+ import { JSDOM, VirtualConsole } from "jsdom";
16
+ import TurndownService from "turndown";
17
+ import { nanoid } from "nanoid";
18
+ import { createServer } from "node:http";
19
+
20
+ //#region src/log.ts
21
+ const LOG_DIR = process.env.ELECTRIC_AGENTS_LOG_DIR ?? path.resolve(process.cwd(), `logs`);
22
+ fsSync.mkdirSync(LOG_DIR, { recursive: true });
23
+ const LOG_FILE = path.join(LOG_DIR, `builtin-agents-${Date.now()}.jsonl`);
24
+ const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
25
+ const USE_PRETTY_LOGS = LOG_LEVEL !== `silent` && !process.env.VITEST;
26
+ const streams = [{ stream: pino.destination(LOG_FILE) }];
27
+ if (USE_PRETTY_LOGS) streams.push({ stream: pino.transport({
28
+ target: `pino-pretty`,
29
+ options: {
30
+ colorize: true,
31
+ ignore: `pid,hostname,name`,
32
+ translateTime: `SYS:HH:MM:ss`
33
+ }
34
+ }) });
35
+ const logger = pino({
36
+ base: void 0,
37
+ level: LOG_LEVEL
38
+ }, pino.multistream(streams));
39
+ function formatArgs(args) {
40
+ const errors = [];
41
+ const parts = [];
42
+ for (const value of args) if (value instanceof Error) errors.push(value);
43
+ else parts.push(typeof value === `string` ? value : JSON.stringify(value));
44
+ return {
45
+ err: errors[0],
46
+ msg: parts.join(` `)
47
+ };
48
+ }
49
+ const serverLog = {
50
+ info(...args) {
51
+ const { msg } = formatArgs(args);
52
+ logger.info(msg);
53
+ },
54
+ warn(...args) {
55
+ const { err, msg } = formatArgs(args);
56
+ if (err) logger.warn({ err }, msg);
57
+ else logger.warn(msg);
58
+ },
59
+ error(...args) {
60
+ const { err, msg } = formatArgs(args);
61
+ if (err) logger.error({ err }, msg);
62
+ else logger.error(msg);
63
+ },
64
+ event(obj, msg) {
65
+ logger.info(obj, msg);
66
+ }
67
+ };
68
+
69
+ //#endregion
70
+ //#region src/docs/embed.ts
71
+ const EMBEDDING_DIMENSIONS = 128;
72
+ function tokenize(value) {
73
+ return value.toLowerCase().replace(/[^a-z0-9\s]/g, ` `).split(/\s+/).filter((token) => token.length > 0);
74
+ }
75
+ function hashToken(token) {
76
+ return createHash(`sha256`).update(token).digest();
77
+ }
78
+ function embedText(value) {
79
+ const vector = new Float32Array(EMBEDDING_DIMENSIONS);
80
+ const tokens = tokenize(value);
81
+ for (const token of tokens) {
82
+ const hash = hashToken(token);
83
+ for (let index = 0; index < 4; index++) {
84
+ const raw = hash.readUInt32BE(index * 4);
85
+ const dimension = raw % EMBEDDING_DIMENSIONS;
86
+ const sign = (hash[index + 16] & 1) === 0 ? 1 : -1;
87
+ vector[dimension] += sign;
88
+ }
89
+ }
90
+ let norm = 0;
91
+ for (const value$1 of vector) norm += value$1 * value$1;
92
+ norm = Math.sqrt(norm);
93
+ if (norm > 0) for (let index = 0; index < vector.length; index++) vector[index] /= norm;
94
+ return vector;
95
+ }
96
+ function embeddingToSqlInput(embedding) {
97
+ return Buffer.from(embedding.buffer.slice(0));
98
+ }
99
+
100
+ //#endregion
101
+ //#region src/docs/knowledge-base.ts
102
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
103
+ const INDEX_VERSION = `1`;
104
+ const DOCS_FINGERPRINT_KEY = `docs_fingerprint`;
105
+ const INDEX_VERSION_KEY = `index_version`;
106
+ const DEFAULT_K = 8;
107
+ function parseFrontmatter(value) {
108
+ if (!value.startsWith(`---\n`)) return { body: value };
109
+ const end = value.indexOf(`\n---\n`, 4);
110
+ if (end === -1) return { body: value };
111
+ const frontmatter = value.slice(4, end);
112
+ const titleMatch = frontmatter.match(/^title:\s*(.+)$/m);
113
+ return {
114
+ title: titleMatch?.[1]?.trim().replace(/^['"]|['"]$/g, ``),
115
+ body: value.slice(end + 5)
116
+ };
117
+ }
118
+ function firstHeading(value) {
119
+ const match = value.match(/^#\s+(.+)$/m);
120
+ return match?.[1]?.trim();
121
+ }
122
+ function collectHeadings(value) {
123
+ const headings = [];
124
+ for (const line of value.split(`\n`)) {
125
+ const match = line.match(/^#{1,6}\s+(.+)$/);
126
+ if (match) headings.push(match[1].trim());
127
+ }
128
+ return headings;
129
+ }
130
+ function toDocTitle(relativePath, content) {
131
+ const parsed = parseFrontmatter(content);
132
+ return parsed.title ?? firstHeading(parsed.body) ?? relativePath.replace(/\.md$/, ``).split(`/`).at(-1) ?? relativePath;
133
+ }
134
+ function normalizeWhitespace(value) {
135
+ return value.replace(/\r\n/g, `\n`).replace(/\n{3,}/g, `\n\n`).trim();
136
+ }
137
+ async function collectMarkdownFiles(root) {
138
+ async function walk(dir) {
139
+ const entries = await fs.readdir(dir, { withFileTypes: true });
140
+ const files = [];
141
+ for (const entry of entries) {
142
+ const fullPath = path.join(dir, entry.name);
143
+ if (entry.isDirectory()) {
144
+ files.push(...await walk(fullPath));
145
+ continue;
146
+ }
147
+ if (entry.isFile() && entry.name.endsWith(`.md`)) files.push(fullPath);
148
+ }
149
+ return files;
150
+ }
151
+ return walk(root);
152
+ }
153
+ function chunkMarkdown(input) {
154
+ const parsed = parseFrontmatter(input.content);
155
+ const title = toDocTitle(input.relativePath, input.content);
156
+ const lines = parsed.body.split(`\n`);
157
+ const sections = [];
158
+ let currentHeading = title;
159
+ let currentLines = [];
160
+ function pushSection() {
161
+ const text = normalizeWhitespace(currentLines.join(`\n`));
162
+ if (!text) return;
163
+ sections.push({
164
+ heading: currentHeading,
165
+ text
166
+ });
167
+ }
168
+ for (const line of lines) {
169
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
170
+ if (headingMatch) {
171
+ pushSection();
172
+ currentHeading = headingMatch[2].trim();
173
+ currentLines = [line];
174
+ continue;
175
+ }
176
+ currentLines.push(line);
177
+ }
178
+ pushSection();
179
+ const chunks = [];
180
+ let chunkIndex = 0;
181
+ for (const section of sections) {
182
+ const paragraphs = section.text.split(/\n\s*\n/).map((paragraph) => normalizeWhitespace(paragraph)).filter(Boolean);
183
+ let current = ``;
184
+ for (const paragraph of paragraphs) {
185
+ const candidate = current ? `${current}\n\n${paragraph}` : paragraph;
186
+ if (candidate.length <= 1200) {
187
+ current = candidate;
188
+ continue;
189
+ }
190
+ if (current) chunks.push({
191
+ docPath: input.relativePath,
192
+ title,
193
+ heading: section.heading,
194
+ chunkIndex: chunkIndex++,
195
+ content: current
196
+ });
197
+ if (paragraph.length <= 1200) {
198
+ current = paragraph;
199
+ continue;
200
+ }
201
+ const overlap = 150;
202
+ let start = 0;
203
+ while (start < paragraph.length) {
204
+ const end = Math.min(start + 1200, paragraph.length);
205
+ const slice = paragraph.slice(start, end).trim();
206
+ if (slice) chunks.push({
207
+ docPath: input.relativePath,
208
+ title,
209
+ heading: section.heading,
210
+ chunkIndex: chunkIndex++,
211
+ content: slice
212
+ });
213
+ if (end >= paragraph.length) {
214
+ current = ``;
215
+ break;
216
+ }
217
+ start = Math.max(end - overlap, start + 1);
218
+ }
219
+ }
220
+ if (current) chunks.push({
221
+ docPath: input.relativePath,
222
+ title,
223
+ heading: section.heading,
224
+ chunkIndex: chunkIndex++,
225
+ content: current
226
+ });
227
+ }
228
+ return chunks;
229
+ }
230
+ function createFingerprint(entries) {
231
+ const hash = createHash(`sha256`);
232
+ for (const entry of entries) {
233
+ hash.update(entry.path);
234
+ hash.update(`\0`);
235
+ hash.update(entry.content);
236
+ hash.update(`\0`);
237
+ }
238
+ return hash.digest(`hex`);
239
+ }
240
+ function getMeta(db, key) {
241
+ const row = db.prepare(`select value from index_meta where key = ?`).get(key);
242
+ return row?.value ?? null;
243
+ }
244
+ function setMeta(db, key, value) {
245
+ db.prepare(`insert into index_meta(key, value) values(?, ?)
246
+ on conflict(key) do update set value = excluded.value`).run(key, value);
247
+ }
248
+ function reciprocalRank(rank, k = 60) {
249
+ return rank === null ? 0 : 1 / (k + rank);
250
+ }
251
+ function sanitizeSnippet(value) {
252
+ return value.replace(/\s+/g, ` `).trim();
253
+ }
254
+ function vectorQuerySql() {
255
+ return `
256
+ select
257
+ id,
258
+ doc_path as docPath,
259
+ title,
260
+ heading,
261
+ content
262
+ from chunks
263
+ order by vec_distance_cosine(embedding, vec_f32(?)) asc
264
+ limit ?
265
+ `;
266
+ }
267
+ function payloadToText(payload) {
268
+ if (typeof payload === `string`) return payload;
269
+ if (payload && typeof payload === `object`) {
270
+ const text = payload.text;
271
+ if (typeof text === `string`) return text;
272
+ return JSON.stringify(payload);
273
+ }
274
+ return String(payload ?? ``);
275
+ }
276
+ function findLatestQuestion(items) {
277
+ for (let index = items.length - 1; index >= 0; index--) {
278
+ const text = payloadToText(items[index]?.payload).trim();
279
+ if (text.length > 0) return text;
280
+ }
281
+ return void 0;
282
+ }
283
+ function resolveDocsRoot(workingDirectory) {
284
+ const candidates = [
285
+ process.env.HORTON_DOCS_ROOT,
286
+ path.resolve(workingDirectory, `electric-agents-docs/docs`),
287
+ path.resolve(process.cwd(), `electric-agents-docs/docs`),
288
+ path.resolve(MODULE_DIR, `../../../../../electric-agents-docs/docs`)
289
+ ].filter((value) => typeof value === `string`);
290
+ for (const candidate of candidates) if (fsSync.existsSync(candidate)) return candidate;
291
+ return null;
292
+ }
293
+ var DocsKnowledgeBase = class {
294
+ db;
295
+ docsRoot;
296
+ dbPath;
297
+ logPrefix;
298
+ fallbackDocs = [];
299
+ fallbackChunks = [];
300
+ fallbackFingerprint = ``;
301
+ readyPromise;
302
+ constructor(options) {
303
+ this.docsRoot = options.docsRoot;
304
+ this.dbPath = options.dbPath;
305
+ this.logPrefix = options.logPrefix ?? `[horton-docs]`;
306
+ this.db = this.openDatabase();
307
+ this.createSchema();
308
+ this.readyPromise = this.ensureIngested();
309
+ }
310
+ openDatabase() {
311
+ fsSync.mkdirSync(path.dirname(this.dbPath), { recursive: true });
312
+ try {
313
+ const db = new Database(this.dbPath);
314
+ load(db);
315
+ db.pragma(`journal_mode = WAL`);
316
+ db.pragma(`synchronous = NORMAL`);
317
+ return db;
318
+ } catch (error) {
319
+ const message = error instanceof Error ? error.message : String(error);
320
+ console.warn(`${this.logPrefix} falling back to in-memory docs index: ${message}`);
321
+ return null;
322
+ }
323
+ }
324
+ createSchema() {
325
+ if (!this.db) return;
326
+ this.db.exec(`
327
+ create table if not exists index_meta (
328
+ key text primary key,
329
+ value text not null
330
+ );
331
+
332
+ create table if not exists docs (
333
+ path text primary key,
334
+ title text not null,
335
+ content text not null
336
+ );
337
+
338
+ create table if not exists chunks (
339
+ id integer primary key,
340
+ doc_path text not null,
341
+ title text not null,
342
+ heading text not null,
343
+ chunk_index integer not null,
344
+ content text not null,
345
+ embedding blob not null check(vec_length(embedding) = ${EMBEDDING_DIMENSIONS}),
346
+ foreign key (doc_path) references docs(path) on delete cascade
347
+ );
348
+
349
+ create index if not exists chunks_doc_path_idx on chunks(doc_path);
350
+
351
+ create virtual table if not exists chunks_fts using fts5(
352
+ doc_path,
353
+ title,
354
+ heading,
355
+ content,
356
+ tokenize = 'porter unicode61'
357
+ );
358
+ `);
359
+ }
360
+ async ensureReady() {
361
+ await this.readyPromise;
362
+ }
363
+ stats() {
364
+ if (!this.db) return {
365
+ docCount: this.fallbackDocs.length,
366
+ chunkCount: this.fallbackChunks.length,
367
+ fingerprint: this.fallbackFingerprint
368
+ };
369
+ const docCount = Number(this.db.prepare(`select count(*) as count from docs`).get().count);
370
+ const chunkCount = Number(this.db.prepare(`select count(*) as count from chunks`).get().count);
371
+ return {
372
+ docCount,
373
+ chunkCount,
374
+ fingerprint: getMeta(this.db, DOCS_FINGERPRINT_KEY) ?? ``
375
+ };
376
+ }
377
+ async ensureIngested() {
378
+ await fs.mkdir(path.dirname(this.dbPath), { recursive: true });
379
+ const files = (await collectMarkdownFiles(this.docsRoot)).sort();
380
+ const docs = await Promise.all(files.map(async (filePath) => ({
381
+ path: path.relative(this.docsRoot, filePath),
382
+ content: await fs.readFile(filePath, `utf8`)
383
+ })));
384
+ const fingerprint = createFingerprint(docs);
385
+ if (!this.db) {
386
+ if (this.fallbackFingerprint === fingerprint && this.fallbackChunks.length > 0) return this.stats();
387
+ this.fallbackDocs = docs.map((doc) => ({
388
+ path: doc.path,
389
+ title: toDocTitle(doc.path, doc.content),
390
+ content: doc.content
391
+ }));
392
+ this.fallbackChunks = [];
393
+ let nextId = 1;
394
+ for (const doc of docs) for (const chunk of chunkMarkdown({
395
+ relativePath: doc.path,
396
+ content: doc.content
397
+ })) {
398
+ const searchableText = `${chunk.title}\n${chunk.heading}\n${chunk.content}`;
399
+ this.fallbackChunks.push({
400
+ ...chunk,
401
+ id: nextId++,
402
+ embedding: embedText(searchableText)
403
+ });
404
+ }
405
+ this.fallbackFingerprint = fingerprint;
406
+ const stats$1 = this.stats();
407
+ console.log(`${this.logPrefix} indexed ${stats$1.docCount} docs into ${stats$1.chunkCount} chunks (${stats$1.fingerprint.slice(0, 12)}...)`);
408
+ return stats$1;
409
+ }
410
+ const db = this.db;
411
+ const currentFingerprint = getMeta(db, DOCS_FINGERPRINT_KEY);
412
+ const currentVersion = getMeta(db, INDEX_VERSION_KEY);
413
+ if (currentFingerprint === fingerprint && currentVersion === INDEX_VERSION && this.stats().chunkCount > 0) return this.stats();
414
+ const insertDoc = db.prepare(`insert into docs(path, title, content) values(?, ?, ?)`);
415
+ const insertChunk = db.prepare(`insert into chunks(doc_path, title, heading, chunk_index, content, embedding)
416
+ values(?, ?, ?, ?, ?, vec_f32(?))`);
417
+ const insertFts = db.prepare(`insert into chunks_fts(rowid, doc_path, title, heading, content)
418
+ values(?, ?, ?, ?, ?)`);
419
+ const reset = db.transaction(() => {
420
+ db.exec(`
421
+ delete from chunks_fts;
422
+ delete from chunks;
423
+ delete from docs;
424
+ `);
425
+ for (const doc of docs) {
426
+ const title = toDocTitle(doc.path, doc.content);
427
+ insertDoc.run(doc.path, title, doc.content);
428
+ const chunks = chunkMarkdown({
429
+ relativePath: doc.path,
430
+ content: doc.content
431
+ });
432
+ for (const chunk of chunks) {
433
+ const searchableText = `${chunk.title}\n${chunk.heading}\n${chunk.content}`;
434
+ const embedding = embeddingToSqlInput(embedText(searchableText));
435
+ const result = insertChunk.run(chunk.docPath, chunk.title, chunk.heading, chunk.chunkIndex, chunk.content, embedding);
436
+ insertFts.run(Number(result.lastInsertRowid), chunk.docPath, chunk.title, chunk.heading, chunk.content);
437
+ }
438
+ }
439
+ setMeta(db, DOCS_FINGERPRINT_KEY, fingerprint);
440
+ setMeta(db, INDEX_VERSION_KEY, INDEX_VERSION);
441
+ });
442
+ reset();
443
+ const stats = this.stats();
444
+ console.log(`${this.logPrefix} indexed ${stats.docCount} docs into ${stats.chunkCount} chunks (${stats.fingerprint.slice(0, 12)}...)`);
445
+ return stats;
446
+ }
447
+ hybridSearch(query, limit = DEFAULT_K) {
448
+ const cleanedQuery = query.trim();
449
+ if (!cleanedQuery) return [];
450
+ if (!this.db) {
451
+ const queryEmbedding = embedText(cleanedQuery);
452
+ const vectorMatches$1 = [...this.fallbackChunks].map((chunk) => ({
453
+ ...chunk,
454
+ vectorScore: dotProduct(queryEmbedding, chunk.embedding)
455
+ })).sort((left, right) => right.vectorScore - left.vectorScore).slice(0, limit * 3);
456
+ const tokens$1 = cleanedQuery.toLowerCase().replace(/[^a-z0-9\s]/g, ` `).split(/\s+/).filter((token) => token.length >= 2);
457
+ const bm25Matches$1 = tokens$1.length > 0 ? [...this.fallbackChunks].map((chunk) => ({
458
+ ...chunk,
459
+ bm25Score: keywordScore(`${chunk.title}\n${chunk.heading}\n${chunk.content}`, tokens$1)
460
+ })).filter((chunk) => chunk.bm25Score > 0).sort((left, right) => right.bm25Score - left.bm25Score).slice(0, limit * 3) : [];
461
+ const merged$1 = new Map();
462
+ for (const [index, row] of bm25Matches$1.entries()) {
463
+ const existing = merged$1.get(row.id);
464
+ merged$1.set(row.id, {
465
+ id: row.id,
466
+ docPath: row.docPath,
467
+ title: row.title,
468
+ heading: row.heading,
469
+ content: row.content,
470
+ hybridScore: reciprocalRank(index + 1) + reciprocalRank(existing?.vectorRank ?? null),
471
+ bm25Rank: index + 1,
472
+ vectorRank: existing?.vectorRank ?? null
473
+ });
474
+ }
475
+ for (const [index, row] of vectorMatches$1.entries()) {
476
+ const existing = merged$1.get(row.id);
477
+ merged$1.set(row.id, {
478
+ id: row.id,
479
+ docPath: row.docPath,
480
+ title: row.title,
481
+ heading: row.heading,
482
+ content: row.content,
483
+ hybridScore: reciprocalRank(existing?.bm25Rank ?? null) + reciprocalRank(index + 1),
484
+ bm25Rank: existing?.bm25Rank ?? null,
485
+ vectorRank: index + 1
486
+ });
487
+ }
488
+ return [...merged$1.values()].sort((left, right) => right.hybridScore - left.hybridScore).slice(0, limit);
489
+ }
490
+ const vectorMatches = this.db.prepare(vectorQuerySql()).all(embeddingToSqlInput(embedText(cleanedQuery)), limit * 3);
491
+ const tokens = cleanedQuery.toLowerCase().replace(/[^a-z0-9\s]/g, ` `).split(/\s+/).filter((token) => token.length >= 2);
492
+ const ftsQuery = tokens.length > 0 ? tokens.map((token) => `"${token}"`).join(` OR `) : null;
493
+ const bm25Matches = ftsQuery ? this.db.prepare(`
494
+ select
495
+ c.id as id,
496
+ c.doc_path as docPath,
497
+ c.title as title,
498
+ c.heading as heading,
499
+ c.content as content,
500
+ bm25(chunks_fts) as bm25Score
501
+ from chunks_fts
502
+ join chunks c on c.id = chunks_fts.rowid
503
+ where chunks_fts match ?
504
+ order by bm25Score
505
+ limit ?
506
+ `).all(ftsQuery, limit * 3) : [];
507
+ const merged = new Map();
508
+ for (const [index, row] of bm25Matches.entries()) {
509
+ const existing = merged.get(row.id);
510
+ merged.set(row.id, {
511
+ id: row.id,
512
+ docPath: row.docPath,
513
+ title: row.title,
514
+ heading: row.heading,
515
+ content: row.content,
516
+ hybridScore: reciprocalRank(index + 1) + reciprocalRank(existing?.vectorRank ?? null),
517
+ bm25Rank: index + 1,
518
+ vectorRank: existing?.vectorRank ?? null
519
+ });
520
+ }
521
+ for (const [index, row] of vectorMatches.entries()) {
522
+ const existing = merged.get(row.id);
523
+ merged.set(row.id, {
524
+ id: row.id,
525
+ docPath: row.docPath,
526
+ title: row.title,
527
+ heading: row.heading,
528
+ content: row.content,
529
+ hybridScore: reciprocalRank(existing?.bm25Rank ?? null) + reciprocalRank(index + 1),
530
+ bm25Rank: existing?.bm25Rank ?? null,
531
+ vectorRank: index + 1
532
+ });
533
+ }
534
+ return [...merged.values()].sort((left, right) => right.hybridScore - left.hybridScore).slice(0, limit);
535
+ }
536
+ outline() {
537
+ if (!this.db) return this.fallbackDocs.map((doc) => ({
538
+ path: doc.path,
539
+ title: doc.title,
540
+ headings: collectHeadings(parseFrontmatter(doc.content).body)
541
+ })).sort((left, right) => left.path.localeCompare(right.path));
542
+ const docs = this.db.prepare(`select path, title, content from docs order by path`).all();
543
+ return docs.map((doc) => ({
544
+ path: doc.path,
545
+ title: doc.title,
546
+ headings: collectHeadings(parseFrontmatter(doc.content).body)
547
+ }));
548
+ }
549
+ renderCompressedToc(maxHeadingsPerDoc = 4) {
550
+ const lines = [`<docs_toc>`];
551
+ for (const doc of this.outline()) {
552
+ const headings = doc.headings.slice(0, maxHeadingsPerDoc).map((heading) => heading.replace(/"/g, `&quot;`));
553
+ const attrs = [`path="${path.resolve(this.docsRoot, doc.path).replace(/"/g, `&quot;`)}"`, `title="${doc.title.replace(/"/g, `&quot;`)}"`];
554
+ if (headings.length > 0) attrs.push(`headings="${headings.join(` | `)}"`);
555
+ if (doc.headings.length > maxHeadingsPerDoc) attrs.push(`more_headings="${doc.headings.length - maxHeadingsPerDoc}"`);
556
+ lines.push(`<doc ${attrs.join(` `)} />`);
557
+ }
558
+ lines.push(`</docs_toc>`);
559
+ return lines.join(`\n`);
560
+ }
561
+ };
562
+ function dotProduct(left, right) {
563
+ let sum = 0;
564
+ for (let index = 0; index < left.length; index++) sum += (left[index] ?? 0) * (right[index] ?? 0);
565
+ return sum;
566
+ }
567
+ function keywordScore(haystack, tokens) {
568
+ const normalized = haystack.toLowerCase();
569
+ let score = 0;
570
+ for (const token of tokens) if (normalized.includes(token)) score += 1;
571
+ return score;
572
+ }
573
+ function renderSearchResults(query, results, docsRoot) {
574
+ if (results.length === 0) return `<docs_search query="${query.replace(/"/g, `&quot;`)}"><no_results /></docs_search>`;
575
+ const lines = [`<docs_search query="${query.replace(/"/g, `&quot;`)}">`];
576
+ for (const [index, result] of results.entries()) {
577
+ const renderedPath = docsRoot ? path.resolve(docsRoot, result.docPath) : result.docPath;
578
+ lines.push(`<chunk rank="${index + 1}" path="${renderedPath.replace(/"/g, `&quot;`)}" title="${result.title.replace(/"/g, `&quot;`)}" heading="${result.heading.replace(/"/g, `&quot;`)}" chunk_id="${result.id}" hybrid_score="${result.hybridScore.toFixed(5)}" bm25_rank="${result.bm25Rank ?? ``}" vector_rank="${result.vectorRank ?? ``}">`, sanitizeSnippet(result.content), `</chunk>`);
579
+ }
580
+ lines.push(`</docs_search>`);
581
+ return lines.join(`\n`);
582
+ }
583
+ function logSearchResults(kind, query, output) {
584
+ console.log(`[horton-docs] ${kind} search for "${query}"\n${output}\n`);
585
+ }
586
+ function createHortonDocsSupport(workingDirectory, opts = {}) {
587
+ const docsRoot = opts.docsRoot ?? resolveDocsRoot(workingDirectory);
588
+ if (!docsRoot) return null;
589
+ const dbPath = opts.dbPath ?? path.resolve(workingDirectory, `.electric-agents/horton-docs.sqlite`);
590
+ const kb = new DocsKnowledgeBase({
591
+ docsRoot,
592
+ dbPath,
593
+ logPrefix: `[horton-docs]`
594
+ });
595
+ function resolveCurrentQuestion(wake, events, inbox) {
596
+ if (wake.type === `message_received`) {
597
+ const eventQuestion = findLatestQuestion(events.filter((event) => event.type === `message_received`).map((event) => event.value));
598
+ if (eventQuestion) return eventQuestion;
599
+ }
600
+ const wakeQuestion = payloadToText(wake.payload).trim();
601
+ if (wakeQuestion.length > 0) return wakeQuestion;
602
+ return findLatestQuestion(inbox) ?? ``;
603
+ }
604
+ return {
605
+ async ensureReady() {
606
+ await kb.ensureReady();
607
+ },
608
+ resolveCurrentQuestion,
609
+ async renderRetrievedDocsSource(wake, events, inbox) {
610
+ await kb.ensureReady();
611
+ const question = resolveCurrentQuestion(wake, events, inbox);
612
+ if (!question) return `<docs_search><no_query /></docs_search>`;
613
+ const rendered = renderSearchResults(question, kb.hybridSearch(question, 6), kb.docsRoot);
614
+ logSearchResults(`initial`, question, rendered);
615
+ return rendered;
616
+ },
617
+ async renderCompressedToc() {
618
+ await kb.ensureReady();
619
+ return kb.renderCompressedToc();
620
+ },
621
+ createSearchTool() {
622
+ return {
623
+ name: `search_durable_agents_docs`,
624
+ label: `Search Durable Agents Docs`,
625
+ description: `Run a hybrid BM25 plus vector search over the local Durable Agents documentation index.`,
626
+ parameters: Type.Object({
627
+ query: Type.String({ description: `The docs question or search query to run.` }),
628
+ limit: Type.Optional(Type.Number({
629
+ minimum: 1,
630
+ maximum: 12,
631
+ description: `Maximum number of chunks to return.`
632
+ }))
633
+ }),
634
+ execute: async (_toolCallId, params) => {
635
+ await kb.ensureReady();
636
+ const query = String(params.query ?? ``).trim();
637
+ const limit = Number(params.limit ?? 6);
638
+ const results = kb.hybridSearch(query, Math.min(Math.max(limit, 1), 12));
639
+ const rendered = renderSearchResults(query, results, kb.docsRoot);
640
+ logSearchResults(`tool`, query, rendered);
641
+ return {
642
+ content: [{
643
+ type: `text`,
644
+ text: rendered
645
+ }],
646
+ details: {
647
+ query,
648
+ resultCount: results.length,
649
+ results: results.map((result) => ({
650
+ id: result.id,
651
+ docPath: result.docPath,
652
+ heading: result.heading,
653
+ hybridScore: result.hybridScore
654
+ }))
655
+ }
656
+ };
657
+ }
658
+ };
659
+ }
660
+ };
661
+ }
662
+
663
+ //#endregion
664
+ //#region src/tools/bash.ts
665
+ const TIMEOUT_MS = 3e4;
666
+ const MAX_OUTPUT_CHARS = 5e4;
667
+ function createBashTool(workingDirectory) {
668
+ return {
669
+ name: `bash`,
670
+ label: `Bash`,
671
+ description: `Execute a shell command and return its output. Commands run in a sandboxed working directory with a 30-second timeout.`,
672
+ parameters: Type.Object({ command: Type.String({ description: `The shell command to execute` }) }),
673
+ execute: async (_toolCallId, params) => {
674
+ const { command } = params;
675
+ return new Promise((resolve$1) => {
676
+ const child = exec(command, {
677
+ cwd: workingDirectory,
678
+ timeout: TIMEOUT_MS,
679
+ maxBuffer: 1024 * 1024,
680
+ env: {
681
+ ...process.env,
682
+ HOME: workingDirectory
683
+ }
684
+ });
685
+ let stdout = ``;
686
+ let stderr = ``;
687
+ child.stdout?.on(`data`, (data) => {
688
+ stdout += data;
689
+ });
690
+ child.stderr?.on(`data`, (data) => {
691
+ stderr += data;
692
+ });
693
+ child.on(`close`, (code, signal) => {
694
+ const timedOut = signal === `SIGTERM`;
695
+ let output = stdout;
696
+ if (stderr) output += output ? `\n\nSTDERR:\n${stderr}` : stderr;
697
+ if (timedOut) output += `\n\n[Command timed out after ${TIMEOUT_MS / 1e3}s]`;
698
+ output = output.slice(0, MAX_OUTPUT_CHARS);
699
+ resolve$1({
700
+ content: [{
701
+ type: `text`,
702
+ text: output || `(no output)`
703
+ }],
704
+ details: {
705
+ exitCode: code ?? 1,
706
+ timedOut
707
+ }
708
+ });
709
+ });
710
+ child.on(`error`, (err) => {
711
+ resolve$1({
712
+ content: [{
713
+ type: `text`,
714
+ text: `Command failed: ${err.message}`
715
+ }],
716
+ details: {
717
+ exitCode: 1,
718
+ timedOut: false
719
+ }
720
+ });
721
+ });
722
+ });
723
+ }
724
+ };
725
+ }
726
+
727
+ //#endregion
728
+ //#region src/tools/edit.ts
729
+ const READ_GUARD_MESSAGE = (rel) => `File ${rel} has not been read in this session (sessions are per-wake — re-read after waking from a worker).`;
730
+ function createEditTool(workingDirectory, readSet) {
731
+ return {
732
+ name: `edit`,
733
+ label: `Edit File`,
734
+ description: `Replace text in a file. The file must have been read with the read tool earlier in this session. By default the old_string must occur exactly once; set replace_all to true to replace every occurrence.`,
735
+ parameters: Type.Object({
736
+ path: Type.String({ description: `File path (relative to working directory)` }),
737
+ old_string: Type.String({ description: `The literal text to find. Must be unique unless replace_all is true.` }),
738
+ new_string: Type.String({ description: `The replacement text.` }),
739
+ replace_all: Type.Optional(Type.Boolean({ description: `Replace every occurrence (default false).` }))
740
+ }),
741
+ execute: async (_toolCallId, params) => {
742
+ const { path: filePath, old_string, new_string, replace_all } = params;
743
+ try {
744
+ const resolved = resolve(workingDirectory, filePath);
745
+ const rel = relative(workingDirectory, resolved);
746
+ if (rel.startsWith(`..`)) return {
747
+ content: [{
748
+ type: `text`,
749
+ text: `Error: Path "${filePath}" is outside the working directory`
750
+ }],
751
+ details: { replacements: 0 }
752
+ };
753
+ if (!readSet.has(resolved)) return {
754
+ content: [{
755
+ type: `text`,
756
+ text: READ_GUARD_MESSAGE(rel)
757
+ }],
758
+ details: { replacements: 0 }
759
+ };
760
+ const original = await readFile(resolved, `utf-8`);
761
+ if (!replace_all) {
762
+ const first = original.indexOf(old_string);
763
+ if (first === -1) return {
764
+ content: [{
765
+ type: `text`,
766
+ text: `Error: old_string not found in ${rel}`
767
+ }],
768
+ details: { replacements: 0 }
769
+ };
770
+ const second = original.indexOf(old_string, first + 1);
771
+ if (second !== -1) {
772
+ const matches = original.split(old_string).length - 1;
773
+ return {
774
+ content: [{
775
+ type: `text`,
776
+ text: `Error: found ${matches} matches for old_string in ${rel}; pass replace_all=true to replace all, or provide a more specific old_string.`
777
+ }],
778
+ details: { replacements: 0 }
779
+ };
780
+ }
781
+ const updated = original.slice(0, first) + new_string + original.slice(first + old_string.length);
782
+ await writeFile(resolved, updated, `utf-8`);
783
+ return {
784
+ content: [{
785
+ type: `text`,
786
+ text: `Edited ${rel}: 1 replacement`
787
+ }],
788
+ details: { replacements: 1 }
789
+ };
790
+ }
791
+ const parts = original.split(old_string);
792
+ const count = parts.length - 1;
793
+ if (count === 0) return {
794
+ content: [{
795
+ type: `text`,
796
+ text: `Error: old_string not found in ${rel}`
797
+ }],
798
+ details: { replacements: 0 }
799
+ };
800
+ await writeFile(resolved, parts.join(new_string), `utf-8`);
801
+ return {
802
+ content: [{
803
+ type: `text`,
804
+ text: `Edited ${rel}: ${count} occurrences replaced`
805
+ }],
806
+ details: { replacements: count }
807
+ };
808
+ } catch (err) {
809
+ serverLog.warn(`[edit tool] failed to edit ${filePath}: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
810
+ return {
811
+ content: [{
812
+ type: `text`,
813
+ text: `Error editing file: ${err instanceof Error ? err.message : `Unknown error`}`
814
+ }],
815
+ details: { replacements: 0 }
816
+ };
817
+ }
818
+ }
819
+ };
820
+ }
821
+
822
+ //#endregion
823
+ //#region src/tools/fetch-url.ts
824
+ const MAX_RAW_CHARS = 1e5;
825
+ const require = createRequire(import.meta.url);
826
+ const { gfm } = require(`turndown-plugin-gfm`);
827
+ function htmlToMarkdown(html, url) {
828
+ const virtualConsole = new VirtualConsole();
829
+ const dom = new JSDOM(html, {
830
+ url,
831
+ virtualConsole
832
+ });
833
+ const reader = new Readability(dom.window.document);
834
+ const article = reader.parse();
835
+ const turndown = new TurndownService({ headingStyle: `atx` });
836
+ turndown.use(gfm);
837
+ return turndown.turndown(article?.content ?? html);
838
+ }
839
+ let anthropic$1 = null;
840
+ function getClient$1() {
841
+ if (!anthropic$1) anthropic$1 = new Anthropic();
842
+ return anthropic$1;
843
+ }
844
+ async function extractWithLLM(text, prompt) {
845
+ const client = getClient$1();
846
+ const res = await client.messages.create({
847
+ model: `claude-haiku-4-5-20251001`,
848
+ max_tokens: 2048,
849
+ messages: [{
850
+ role: `user`,
851
+ content: `${prompt}\n\n<page_content>\n${text.slice(0, MAX_RAW_CHARS)}\n</page_content>`
852
+ }]
853
+ });
854
+ const block = res.content[0];
855
+ return block?.type === `text` ? block.text : ``;
856
+ }
857
+ const fetchUrlTool = {
858
+ name: `fetch_url`,
859
+ label: `Fetch URL`,
860
+ description: `Fetch a web page and extract its key content using AI. Provide a prompt describing what information you want from the page. Returns a focused extraction rather than raw HTML.`,
861
+ parameters: Type.Object({
862
+ url: Type.String({ description: `The URL to fetch` }),
863
+ prompt: Type.String({ description: `What to extract from the page, e.g. 'Extract the main article content' or 'Find the pricing information'` })
864
+ }),
865
+ execute: async (_toolCallId, params) => {
866
+ const { url, prompt } = params;
867
+ try {
868
+ const res = await fetch(url, {
869
+ headers: {
870
+ "User-Agent": `Mozilla/5.0 (compatible; DurableStreamsAgent/1.0)`,
871
+ Accept: `text/html,application/xhtml+xml,text/plain,*/*`
872
+ },
873
+ redirect: `follow`,
874
+ signal: AbortSignal.timeout(1e4)
875
+ });
876
+ if (!res.ok) return {
877
+ content: [{
878
+ type: `text`,
879
+ text: `Failed to fetch: ${res.status} ${res.statusText}`
880
+ }],
881
+ details: {
882
+ charCount: 0,
883
+ usedLLM: false
884
+ }
885
+ };
886
+ const contentType = res.headers.get(`content-type`) ?? ``;
887
+ const raw = await res.text();
888
+ const markdown = contentType.includes(`text/html`) ? htmlToMarkdown(raw, url) : raw;
889
+ const extracted = await extractWithLLM(markdown, prompt);
890
+ return {
891
+ content: [{
892
+ type: `text`,
893
+ text: extracted
894
+ }],
895
+ details: {
896
+ charCount: extracted.length,
897
+ usedLLM: true
898
+ }
899
+ };
900
+ } catch (err) {
901
+ return {
902
+ content: [{
903
+ type: `text`,
904
+ text: `Error fetching URL: ${err instanceof Error ? err.message : `Unknown error`}`
905
+ }],
906
+ details: {
907
+ charCount: 0,
908
+ usedLLM: false
909
+ }
910
+ };
911
+ }
912
+ }
913
+ };
914
+
915
+ //#endregion
916
+ //#region src/tools/read-file.ts
917
+ const MAX_FILE_SIZE = 512 * 1024;
918
+ function createReadFileTool(workingDirectory, readSet) {
919
+ return {
920
+ name: `read`,
921
+ label: `Read File`,
922
+ description: `Read the contents of a file. Path must be relative to or within the working directory. Binary files and files over 512KB are rejected.`,
923
+ parameters: Type.Object({ path: Type.String({ description: `File path (relative to working directory)` }) }),
924
+ execute: async (_toolCallId, params) => {
925
+ const { path: filePath } = params;
926
+ try {
927
+ const resolved = resolve(workingDirectory, filePath);
928
+ const rel = relative(workingDirectory, resolved);
929
+ if (rel.startsWith(`..`)) return {
930
+ content: [{
931
+ type: `text`,
932
+ text: `Error: Path "${filePath}" is outside the working directory`
933
+ }],
934
+ details: { charCount: 0 }
935
+ };
936
+ const fileStat = await stat(resolved);
937
+ if (fileStat.size > MAX_FILE_SIZE) return {
938
+ content: [{
939
+ type: `text`,
940
+ text: `Error: File is too large (${(fileStat.size / 1024).toFixed(0)}KB > ${MAX_FILE_SIZE / 1024}KB limit)`
941
+ }],
942
+ details: { charCount: 0 }
943
+ };
944
+ const buffer = await readFile(resolved);
945
+ const sample = buffer.subarray(0, 8192);
946
+ if (sample.includes(0)) return {
947
+ content: [{
948
+ type: `text`,
949
+ text: `Error: "${filePath}" appears to be a binary file`
950
+ }],
951
+ details: { charCount: 0 }
952
+ };
953
+ const text = buffer.toString(`utf-8`);
954
+ readSet?.add(resolved);
955
+ return {
956
+ content: [{
957
+ type: `text`,
958
+ text
959
+ }],
960
+ details: { charCount: text.length }
961
+ };
962
+ } catch (err) {
963
+ serverLog.warn(`[read tool] failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
964
+ return {
965
+ content: [{
966
+ type: `text`,
967
+ text: `Error reading file: ${err instanceof Error ? err.message : `Unknown error`}`
968
+ }],
969
+ details: { charCount: 0 }
970
+ };
971
+ }
972
+ }
973
+ };
974
+ }
975
+
976
+ //#endregion
977
+ //#region src/tools/spawn-worker.ts
978
+ const WORKER_TOOL_NAMES = [
979
+ `bash`,
980
+ `read`,
981
+ `write`,
982
+ `edit`,
983
+ `brave_search`,
984
+ `fetch_url`,
985
+ `spawn_worker`
986
+ ];
987
+ function createSpawnWorkerTool(ctx) {
988
+ return {
989
+ name: `spawn_worker`,
990
+ label: `Spawn Worker`,
991
+ description: `Dispatch a subagent (worker) to perform an isolated subtask. Provide a system prompt that briefs the worker like a colleague who just walked into the room (file paths, line numbers, what specifically to do, what form of answer you want back) and pick the subset of tools the worker needs.`,
992
+ parameters: Type.Object({
993
+ systemPrompt: Type.String({ description: `System prompt for the worker. Be concrete: include file paths, line numbers, and the form of answer you want back.` }),
994
+ tools: Type.Array(Type.Union(WORKER_TOOL_NAMES.map((n) => Type.Literal(n))), { description: `Subset of tool names to enable for the worker. Must include at least one.` }),
995
+ initialMessage: Type.String({ description: `First user message sent to the worker. This is what kicks off its run — without it the worker will idle. Describe the concrete task to perform.` })
996
+ }),
997
+ execute: async (_toolCallId, params) => {
998
+ const { systemPrompt, tools, initialMessage } = params;
999
+ if (!Array.isArray(tools) || tools.length === 0) return {
1000
+ content: [{
1001
+ type: `text`,
1002
+ text: `Error: provide at least one tool for the worker.`
1003
+ }],
1004
+ details: { spawned: false }
1005
+ };
1006
+ if (typeof initialMessage !== `string` || initialMessage.length === 0) return {
1007
+ content: [{
1008
+ type: `text`,
1009
+ text: `Error: initialMessage is required and must be a non-empty string.`
1010
+ }],
1011
+ details: { spawned: false }
1012
+ };
1013
+ const id = nanoid(10);
1014
+ try {
1015
+ const handle = await ctx.spawn(`worker`, id, {
1016
+ systemPrompt,
1017
+ tools
1018
+ }, {
1019
+ initialMessage,
1020
+ wake: {
1021
+ on: `runFinished`,
1022
+ includeResponse: true
1023
+ }
1024
+ });
1025
+ const workerUrl = handle.entityUrl;
1026
+ return {
1027
+ content: [{
1028
+ type: `text`,
1029
+ text: `Worker dispatched at ${workerUrl}. End your turn — when you next wake, the wake message will tell you the worker has finished and include its response.`
1030
+ }],
1031
+ details: {
1032
+ spawned: true,
1033
+ workerUrl
1034
+ }
1035
+ };
1036
+ } catch (err) {
1037
+ serverLog.warn(`[spawn_worker tool] failed to spawn worker ${id}: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
1038
+ return {
1039
+ content: [{
1040
+ type: `text`,
1041
+ text: `Error spawning worker: ${err instanceof Error ? err.message : `Unknown error`}`
1042
+ }],
1043
+ details: { spawned: false }
1044
+ };
1045
+ }
1046
+ }
1047
+ };
1048
+ }
1049
+
1050
+ //#endregion
1051
+ //#region src/tools/write.ts
1052
+ function createWriteTool(workingDirectory, readSet) {
1053
+ return {
1054
+ name: `write`,
1055
+ label: `Write File`,
1056
+ description: `Create or overwrite a file. Path must be within the working directory. Parent directories are created as needed.`,
1057
+ parameters: Type.Object({
1058
+ path: Type.String({ description: `File path (relative to working directory)` }),
1059
+ content: Type.String({ description: `Full file contents to write` })
1060
+ }),
1061
+ execute: async (_toolCallId, params) => {
1062
+ const { path: filePath, content } = params;
1063
+ try {
1064
+ const resolved = resolve(workingDirectory, filePath);
1065
+ const rel = relative(workingDirectory, resolved);
1066
+ if (rel.startsWith(`..`)) return {
1067
+ content: [{
1068
+ type: `text`,
1069
+ text: `Error: Path "${filePath}" is outside the working directory`
1070
+ }],
1071
+ details: { bytesWritten: 0 }
1072
+ };
1073
+ await mkdir(dirname(resolved), { recursive: true });
1074
+ await writeFile(resolved, content, `utf-8`);
1075
+ readSet?.add(resolved);
1076
+ const bytesWritten = Buffer.byteLength(content, `utf-8`);
1077
+ return {
1078
+ content: [{
1079
+ type: `text`,
1080
+ text: `Wrote ${bytesWritten} bytes to ${rel}`
1081
+ }],
1082
+ details: { bytesWritten }
1083
+ };
1084
+ } catch (err) {
1085
+ serverLog.warn(`[write tool] failed to write ${filePath}: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
1086
+ return {
1087
+ content: [{
1088
+ type: `text`,
1089
+ text: `Error writing file: ${err instanceof Error ? err.message : `Unknown error`}`
1090
+ }],
1091
+ details: { bytesWritten: 0 }
1092
+ };
1093
+ }
1094
+ }
1095
+ };
1096
+ }
1097
+
1098
+ //#endregion
1099
+ //#region src/tools/brave-search.ts
1100
+ const BRAVE_API_URL = `https://api.search.brave.com/res/v1/web/search`;
1101
+ const braveSearchTool = {
1102
+ name: `web_search`,
1103
+ label: `Web Search`,
1104
+ description: `Search the web for current information using Brave Search. Returns titles, URLs, and snippets from top results.`,
1105
+ parameters: Type.Object({ query: Type.String({ description: `The search query` }) }),
1106
+ execute: async (_toolCallId, params) => {
1107
+ const apiKey = process.env.BRAVE_SEARCH_API_KEY;
1108
+ if (!apiKey) return {
1109
+ content: [{
1110
+ type: `text`,
1111
+ text: `Search failed: BRAVE_SEARCH_API_KEY not set`
1112
+ }],
1113
+ details: { resultCount: 0 }
1114
+ };
1115
+ const { query } = params;
1116
+ try {
1117
+ const url = `${BRAVE_API_URL}?q=${encodeURIComponent(query)}&count=5`;
1118
+ const res = await fetch(url, { headers: { "X-Subscription-Token": apiKey } });
1119
+ if (!res.ok) return {
1120
+ content: [{
1121
+ type: `text`,
1122
+ text: `Search failed: ${res.status} ${res.statusText}`
1123
+ }],
1124
+ details: { resultCount: 0 }
1125
+ };
1126
+ const data = await res.json();
1127
+ const results = data.web?.results ?? [];
1128
+ if (results.length === 0) return {
1129
+ content: [{
1130
+ type: `text`,
1131
+ text: `No results found for "${query}"`
1132
+ }],
1133
+ details: { resultCount: 0 }
1134
+ };
1135
+ const formatted = results.map((r, i) => `${i + 1}. **${r.title}**\n ${r.url}\n ${r.description}`).join(`\n\n`);
1136
+ return {
1137
+ content: [{
1138
+ type: `text`,
1139
+ text: formatted
1140
+ }],
1141
+ details: { resultCount: results.length }
1142
+ };
1143
+ } catch (err) {
1144
+ return {
1145
+ content: [{
1146
+ type: `text`,
1147
+ text: `Search failed: ${err instanceof Error ? err.message : `Unknown error`}`
1148
+ }],
1149
+ details: { resultCount: 0 }
1150
+ };
1151
+ }
1152
+ }
1153
+ };
1154
+
1155
+ //#endregion
1156
+ //#region src/agents/horton.ts
1157
+ const TITLE_MODEL = `claude-haiku-4-5-20251001`;
1158
+ const HORTON_MODEL = `claude-sonnet-4-5-20250929`;
1159
+ let anthropic = null;
1160
+ function getClient() {
1161
+ if (!anthropic) anthropic = new Anthropic();
1162
+ return anthropic;
1163
+ }
1164
+ async function defaultHaikuCall(prompt) {
1165
+ const client = getClient();
1166
+ const res = await client.messages.create({
1167
+ model: TITLE_MODEL,
1168
+ max_tokens: 64,
1169
+ messages: [{
1170
+ role: `user`,
1171
+ content: prompt
1172
+ }]
1173
+ });
1174
+ const block = res.content[0];
1175
+ return block?.type === `text` ? block.text : ``;
1176
+ }
1177
+ const TITLE_PROMPT = (userMessage) => `Summarize the following user request in 3-5 words for use as a chat session title.
1178
+ Respond with only the title, no quotes, no punctuation, no preamble.
1179
+
1180
+ User request:
1181
+ ${userMessage}`;
1182
+ const TITLE_STOP_WORDS = new Set([
1183
+ `a`,
1184
+ `an`,
1185
+ `and`,
1186
+ `are`,
1187
+ `can`,
1188
+ `for`,
1189
+ `from`,
1190
+ `help`,
1191
+ `i`,
1192
+ `in`,
1193
+ `into`,
1194
+ `is`,
1195
+ `it`,
1196
+ `look`,
1197
+ `me`,
1198
+ `my`,
1199
+ `need`,
1200
+ `of`,
1201
+ `on`,
1202
+ `or`,
1203
+ `please`,
1204
+ `the`,
1205
+ `this`,
1206
+ `to`,
1207
+ `we`,
1208
+ `with`,
1209
+ `you`
1210
+ ]);
1211
+ const TITLE_IGNORED_WORDS = new Set([
1212
+ `dist`,
1213
+ `js`,
1214
+ `json`,
1215
+ `jsx`,
1216
+ `md`,
1217
+ `package`,
1218
+ `packages`,
1219
+ `src`,
1220
+ `ts`,
1221
+ `tsx`,
1222
+ `yaml`,
1223
+ `yml`
1224
+ ]);
1225
+ function toTitleWord(word) {
1226
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
1227
+ }
1228
+ function buildFallbackTitle(userMessage) {
1229
+ const cleaned = userMessage.replace(/`[^`]*`/g, ` `).replace(/[./\\_-]+/g, ` `).replace(/[^a-zA-Z0-9\s]/g, ` `).replace(/\s+/g, ` `).trim();
1230
+ const rawWords = cleaned.split(/\s+/).filter(Boolean);
1231
+ const seen = new Set();
1232
+ const informativeWords = [];
1233
+ const backupWords = [];
1234
+ for (const rawWord of rawWords) {
1235
+ const word = rawWord.toLowerCase();
1236
+ if (seen.has(word)) continue;
1237
+ seen.add(word);
1238
+ if (!/^[a-z0-9]+$/i.test(word)) continue;
1239
+ if (word.length < 2 || /^\d+$/.test(word)) continue;
1240
+ if (TITLE_IGNORED_WORDS.has(word)) continue;
1241
+ const titled = toTitleWord(word);
1242
+ if (backupWords.length < 5) backupWords.push(titled);
1243
+ if (TITLE_STOP_WORDS.has(word)) continue;
1244
+ informativeWords.push(titled);
1245
+ }
1246
+ const selected = informativeWords.length >= 2 ? informativeWords.slice(0, 5) : backupWords;
1247
+ return selected.join(` `).slice(0, 80).trim() || `Untitled Chat`;
1248
+ }
1249
+ async function generateTitle(userMessage, llmCall = defaultHaikuCall) {
1250
+ try {
1251
+ const raw = await llmCall(TITLE_PROMPT(userMessage));
1252
+ const title = raw.trim();
1253
+ return title.length > 0 ? title : buildFallbackTitle(userMessage);
1254
+ } catch {
1255
+ return buildFallbackTitle(userMessage);
1256
+ }
1257
+ }
1258
+ function buildHortonSystemPrompt(workingDirectory, opts = {}) {
1259
+ const docsTools = opts.hasDocsSupport ? `\n- search_durable_agents_docs: hybrid search over the built-in Durable Agents docs index` : ``;
1260
+ 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.` : ``;
1261
+ return `You are Horton, a friendly and capable assistant. You can chat, research the web, read and edit code, run shell commands, and dispatch subagents (workers) for isolated subtasks. Be warm and engaging in conversation; be precise and concrete when working with code.
1262
+
1263
+ # Tools
1264
+ - bash: run shell commands
1265
+ - read: read a file
1266
+ - write: create or overwrite a file
1267
+ - edit: targeted string replacement in an existing file (you must read the file first)
1268
+ - brave_search: search the web
1269
+ - fetch_url: fetch and convert a URL to markdown
1270
+ - spawn_worker: dispatch a subagent for an isolated task
1271
+ ${docsTools}
1272
+
1273
+ # Working with files
1274
+ - Prefer edit over write when modifying existing files.
1275
+ - You must read a file before you can edit it.
1276
+ - Use absolute paths or paths relative to the current working directory.
1277
+ ${docsGuidance}
1278
+
1279
+ # Risky actions
1280
+ Pause and confirm with the user before:
1281
+ - Destructive operations (deleting files, rm -rf, dropping data, force-pushing)
1282
+ - Hard-to-reverse operations (git reset --hard, removing dependencies)
1283
+ - Actions visible to others (pushing code, opening PRs, sending messages)
1284
+
1285
+ # Parallelism
1286
+ Run independent tool calls in parallel. Only run sequentially when one call depends on the result of another.
1287
+
1288
+ # When to spawn a worker
1289
+ Dispatch a worker when:
1290
+ - The subtask involves long research that would clutter our conversation.
1291
+ - The subtask is independent and can run in parallel with other work.
1292
+ - You need an isolated context (e.g., focused coding on one file without pulling its full content into our chat).
1293
+
1294
+ When you spawn a worker, write its system prompt the way you'd brief a colleague who just walked in: include file paths, line numbers, what specifically to do, and what form of answer you want back. The system prompt sets the worker's persona and constraints; the required initialMessage is the concrete task you're handing off — that's what kicks the worker off, so without it the worker sits idle.
1295
+
1296
+ After spawning, end your turn (optionally with a brief "I've dispatched a worker for X; I'll respond when it finishes"). When the worker finishes, you'll receive a message describing which worker completed and what it returned. Multiple workers may finish at different times — check the message for the worker URL to know which one you're hearing about.
1297
+
1298
+ # Reporting
1299
+ Report outcomes faithfully. If a command failed, say so with the relevant output. If you didn't run a verification step, say that rather than implying you did. Don't hedge confirmed results with unnecessary disclaimers.
1300
+
1301
+ Working directory: ${workingDirectory}
1302
+ The current year is ${new Date().getFullYear()}.`;
1303
+ }
1304
+ function createHortonTools(workingDirectory, ctx, readSet, opts = {}) {
1305
+ return [
1306
+ createBashTool(workingDirectory),
1307
+ createReadFileTool(workingDirectory, readSet),
1308
+ createWriteTool(workingDirectory, readSet),
1309
+ createEditTool(workingDirectory, readSet),
1310
+ braveSearchTool,
1311
+ fetchUrlTool,
1312
+ createSpawnWorkerTool(ctx),
1313
+ ...opts.docsSearchTool ? [opts.docsSearchTool] : []
1314
+ ];
1315
+ }
1316
+ function extractFirstUserMessage(events) {
1317
+ for (const event of events) {
1318
+ if (event.type !== `message_received`) continue;
1319
+ const value = event.value;
1320
+ if (!value || value.from === `system`) continue;
1321
+ const payload = value.payload;
1322
+ if (typeof payload === `string`) return payload;
1323
+ if (payload != null) return JSON.stringify(payload);
1324
+ }
1325
+ return null;
1326
+ }
1327
+ function createAssistantHandler(options) {
1328
+ const { workingDirectory, streamFn, docsSupport, docsSearchTool } = options;
1329
+ return async function assistantHandler(ctx, wake) {
1330
+ const readSet = new Set();
1331
+ const tools = [...ctx.electricTools, ...createHortonTools(workingDirectory, ctx, readSet, { docsSearchTool })];
1332
+ if (docsSupport) ctx.useContext({
1333
+ sourceBudget: 1e5,
1334
+ sources: {
1335
+ docs_toc: {
1336
+ content: () => docsSupport.renderCompressedToc(),
1337
+ max: 3e3,
1338
+ cache: `stable`
1339
+ },
1340
+ retrieved_docs: {
1341
+ content: () => docsSupport.renderRetrievedDocsSource(wake, ctx.events, ctx.db.collections.inbox.toArray),
1342
+ max: 6e3,
1343
+ cache: `volatile`
1344
+ },
1345
+ conversation: {
1346
+ content: () => ctx.timelineMessages(),
1347
+ cache: `volatile`
1348
+ }
1349
+ }
1350
+ });
1351
+ ctx.useAgent({
1352
+ systemPrompt: buildHortonSystemPrompt(workingDirectory, { hasDocsSupport: Boolean(docsSupport) }),
1353
+ model: HORTON_MODEL,
1354
+ tools,
1355
+ ...streamFn && { streamFn }
1356
+ });
1357
+ await ctx.agent.run();
1358
+ if (ctx.firstWake && !ctx.tags.title) {
1359
+ const firstUserMessage = extractFirstUserMessage(ctx.events);
1360
+ if (firstUserMessage) {
1361
+ let title = null;
1362
+ try {
1363
+ const result = await generateTitle(firstUserMessage);
1364
+ if (result.length > 0) title = result;
1365
+ } catch (err) {
1366
+ serverLog.warn(`[horton ${ctx.entityUrl}] title generation failed: ${err instanceof Error ? err.message : String(err)}`);
1367
+ }
1368
+ if (title !== null) try {
1369
+ await ctx.setTag(`title`, title);
1370
+ } catch (err) {
1371
+ serverLog.warn(`[horton ${ctx.entityUrl}] setTag failed: ${err instanceof Error ? err.message : String(err)}`);
1372
+ }
1373
+ }
1374
+ }
1375
+ };
1376
+ }
1377
+ function registerHorton(registry, options) {
1378
+ const { workingDirectory, streamFn } = options;
1379
+ const docsSupport = createHortonDocsSupport(workingDirectory);
1380
+ const docsSearchTool = docsSupport?.createSearchTool();
1381
+ docsSupport?.ensureReady().catch((error) => {
1382
+ serverLog.warn(`[horton-docs] warmup failed: ${error instanceof Error ? error.message : String(error)}`);
1383
+ });
1384
+ const assistantHandler = createAssistantHandler({
1385
+ workingDirectory,
1386
+ streamFn,
1387
+ docsSupport,
1388
+ docsSearchTool
1389
+ });
1390
+ registry.define(`horton`, {
1391
+ description: `Friendly capable assistant — chat, code, research, dispatch`,
1392
+ handler: assistantHandler
1393
+ });
1394
+ const typeNames = [`horton`];
1395
+ if (streamFn) {
1396
+ registry.define(`chat`, {
1397
+ description: `Compatibility alias for the built-in assistant type.`,
1398
+ handler: assistantHandler
1399
+ });
1400
+ typeNames.push(`chat`);
1401
+ }
1402
+ return typeNames;
1403
+ }
1404
+
1405
+ //#endregion
1406
+ //#region src/agents/worker.ts
1407
+ function isWorkerToolName(value) {
1408
+ return typeof value === `string` && WORKER_TOOL_NAMES.includes(value);
1409
+ }
1410
+ function parseWorkerArgs(value) {
1411
+ if (typeof value.systemPrompt !== `string` || value.systemPrompt.length === 0) throw new Error(`[worker] systemPrompt is required`);
1412
+ if (!Array.isArray(value.tools) || value.tools.length === 0) throw new Error(`[worker] tools must be a non-empty array`);
1413
+ const tools = [];
1414
+ for (const t of value.tools) {
1415
+ if (!isWorkerToolName(t)) throw new Error(`[worker] unknown tool name: ${JSON.stringify(t)}. Valid tools: ${WORKER_TOOL_NAMES.join(`, `)}`);
1416
+ if (!tools.includes(t)) tools.push(t);
1417
+ }
1418
+ return {
1419
+ systemPrompt: value.systemPrompt,
1420
+ tools
1421
+ };
1422
+ }
1423
+ function buildToolsForWorker(tools, workingDirectory, ctx, readSet) {
1424
+ const out = [];
1425
+ for (const name of tools) switch (name) {
1426
+ case `bash`:
1427
+ out.push(createBashTool(workingDirectory));
1428
+ break;
1429
+ case `read`:
1430
+ out.push(createReadFileTool(workingDirectory, readSet));
1431
+ break;
1432
+ case `write`:
1433
+ out.push(createWriteTool(workingDirectory, readSet));
1434
+ break;
1435
+ case `edit`:
1436
+ out.push(createEditTool(workingDirectory, readSet));
1437
+ break;
1438
+ case `brave_search`:
1439
+ out.push(braveSearchTool);
1440
+ break;
1441
+ case `fetch_url`:
1442
+ out.push(fetchUrlTool);
1443
+ break;
1444
+ case `spawn_worker`:
1445
+ out.push(createSpawnWorkerTool(ctx));
1446
+ break;
1447
+ }
1448
+ return out;
1449
+ }
1450
+ const WORKER_PROMPT_FOOTER = `
1451
+
1452
+ # Reporting back
1453
+ 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.`;
1454
+ function registerWorker(registry, options) {
1455
+ const { workingDirectory, streamFn } = options;
1456
+ registry.define(`worker`, {
1457
+ description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools).`,
1458
+ async handler(ctx) {
1459
+ const args = parseWorkerArgs(ctx.args);
1460
+ const readSet = new Set();
1461
+ const tools = buildToolsForWorker(args.tools, workingDirectory, ctx, readSet);
1462
+ ctx.useAgent({
1463
+ systemPrompt: `${args.systemPrompt}${WORKER_PROMPT_FOOTER}`,
1464
+ model: HORTON_MODEL,
1465
+ tools,
1466
+ ...streamFn && { streamFn }
1467
+ });
1468
+ await ctx.agent.run();
1469
+ }
1470
+ });
1471
+ }
1472
+
1473
+ //#endregion
1474
+ //#region src/bootstrap.ts
1475
+ const DEFAULT_BUILTIN_AGENT_HANDLER_PATH = `/_electric/builtin-agent-handler`;
1476
+ function createBuiltinAgentHandler(options) {
1477
+ const { agentServerUrl, serveEndpoint = `${agentServerUrl}${DEFAULT_BUILTIN_AGENT_HANDLER_PATH}`, workingDirectory, streamFn, createElectricTools } = options;
1478
+ if (!streamFn && !process.env.ANTHROPIC_API_KEY) {
1479
+ serverLog.warn(`[builtin-agents] ANTHROPIC_API_KEY not set — skipping built-in agent registration`);
1480
+ return null;
1481
+ }
1482
+ const cwd = workingDirectory ?? process.cwd();
1483
+ const registry = createEntityRegistry();
1484
+ const typeNames = registerHorton(registry, {
1485
+ workingDirectory: cwd,
1486
+ streamFn
1487
+ });
1488
+ registerWorker(registry, {
1489
+ workingDirectory: cwd,
1490
+ streamFn
1491
+ });
1492
+ typeNames.push(`worker`);
1493
+ const runtime = createRuntimeHandler({
1494
+ baseUrl: agentServerUrl,
1495
+ serveEndpoint,
1496
+ registry,
1497
+ subscriptionPathForType: (name) => `/${name}/*/main`,
1498
+ idleTimeout: 5e3,
1499
+ createElectricTools
1500
+ });
1501
+ return {
1502
+ handler: runtime.onEnter,
1503
+ runtime,
1504
+ registry,
1505
+ typeNames
1506
+ };
1507
+ }
1508
+ function createAgentHandler(agentServerUrl, workingDirectory, streamFn, createElectricTools, serveEndpoint) {
1509
+ return createBuiltinAgentHandler({
1510
+ agentServerUrl,
1511
+ serveEndpoint,
1512
+ workingDirectory,
1513
+ streamFn,
1514
+ createElectricTools
1515
+ });
1516
+ }
1517
+ async function registerBuiltinAgentTypes(bootstrap) {
1518
+ await bootstrap.runtime.registerTypes();
1519
+ serverLog.info(`[builtin-agents] ${bootstrap.typeNames.length} built-in agent types ready: ${bootstrap.typeNames.join(`, `)}`);
1520
+ }
1521
+ const registerAgentTypes = registerBuiltinAgentTypes;
1522
+
1523
+ //#endregion
1524
+ //#region src/server.ts
1525
+ var BuiltinAgentsServer = class {
1526
+ server = null;
1527
+ bootstrap = null;
1528
+ _url = null;
1529
+ publicBaseUrl = null;
1530
+ options;
1531
+ constructor(options) {
1532
+ this.options = options;
1533
+ }
1534
+ get url() {
1535
+ if (!this._url) throw new Error(`Builtin agents server not started`);
1536
+ return this._url;
1537
+ }
1538
+ get registeredBaseUrl() {
1539
+ if (!this.publicBaseUrl) throw new Error(`Builtin agents server not started`);
1540
+ return this.publicBaseUrl;
1541
+ }
1542
+ async start() {
1543
+ if (this.server) throw new Error(`Builtin agents server already started`);
1544
+ return new Promise((resolve$1, reject) => {
1545
+ this.server = createServer((req, res) => {
1546
+ this.handleRequest(req, res).catch((error) => {
1547
+ serverLog.error(`[builtin-agents] unhandled request error`, error);
1548
+ if (!res.headersSent) {
1549
+ res.writeHead(500, { "content-type": `application/json` });
1550
+ res.end(JSON.stringify({ error: `Internal server error` }));
1551
+ }
1552
+ });
1553
+ });
1554
+ this.server.on(`error`, reject);
1555
+ const host = this.options.host ?? `127.0.0.1`;
1556
+ this.server.listen(this.options.port, host, async () => {
1557
+ try {
1558
+ const addr = this.server.address();
1559
+ if (typeof addr === `string`) this._url = addr;
1560
+ else if (addr) {
1561
+ const resolvedHost = host === `0.0.0.0` ? `127.0.0.1` : host;
1562
+ this._url = `http://${resolvedHost}:${addr.port}`;
1563
+ } else throw new Error(`Could not determine builtin agents server address`);
1564
+ this.publicBaseUrl = this.options.baseUrl ?? this._url;
1565
+ const webhookPath = this.options.webhookPath ?? DEFAULT_BUILTIN_AGENT_HANDLER_PATH;
1566
+ const serveEndpoint = new URL(webhookPath, this.publicBaseUrl.endsWith(`/`) ? this.publicBaseUrl : `${this.publicBaseUrl}/`).toString();
1567
+ this.bootstrap = createBuiltinAgentHandler({
1568
+ agentServerUrl: this.options.agentServerUrl,
1569
+ serveEndpoint,
1570
+ workingDirectory: this.options.workingDirectory,
1571
+ streamFn: this.options.mockStreamFn,
1572
+ createElectricTools: this.options.createElectricTools
1573
+ });
1574
+ if (!this.bootstrap) throw new Error(`ANTHROPIC_API_KEY must be set before starting builtin agents`);
1575
+ await registerBuiltinAgentTypes(this.bootstrap);
1576
+ serverLog.info(`[builtin-agents] webhook handler listening at ${serveEndpoint}`);
1577
+ resolve$1(this._url);
1578
+ } catch (error) {
1579
+ await this.stop().catch(() => {});
1580
+ reject(error);
1581
+ }
1582
+ });
1583
+ });
1584
+ }
1585
+ async stop() {
1586
+ if (this.bootstrap) {
1587
+ this.bootstrap.runtime.abortWakes();
1588
+ await Promise.race([this.bootstrap.runtime.drainWakes().catch(() => {}), new Promise((resolve$1) => setTimeout(resolve$1, 5e3))]);
1589
+ this.bootstrap = null;
1590
+ }
1591
+ if (this.server) {
1592
+ const server = this.server;
1593
+ await new Promise((resolve$1) => {
1594
+ server.close(() => resolve$1());
1595
+ });
1596
+ this.server = null;
1597
+ }
1598
+ this._url = null;
1599
+ this.publicBaseUrl = null;
1600
+ }
1601
+ async handleRequest(req, res) {
1602
+ const method = req.method?.toUpperCase();
1603
+ const path$1 = new URL(req.url ?? `/`, `http://localhost`).pathname;
1604
+ const webhookPath = this.options.webhookPath ?? DEFAULT_BUILTIN_AGENT_HANDLER_PATH;
1605
+ if (path$1 === `/_electric/health` && method === `GET`) {
1606
+ res.writeHead(200, { "content-type": `application/json` });
1607
+ res.end(JSON.stringify({ status: `ok` }));
1608
+ return;
1609
+ }
1610
+ if (path$1 === webhookPath && method === `POST` && this.bootstrap) {
1611
+ await this.bootstrap.handler(req, res);
1612
+ return;
1613
+ }
1614
+ res.writeHead(404, { "content-type": `application/json` });
1615
+ res.end(JSON.stringify({ error: `Not found` }));
1616
+ }
1617
+ };
1618
+
1619
+ //#endregion
1620
+ //#region src/entrypoint-lib.ts
1621
+ const DEFAULT_HOST = `127.0.0.1`;
1622
+ const DEFAULT_PORT = 4448;
1623
+ function readEnv(env, names) {
1624
+ for (const name of names) {
1625
+ const value = env[name]?.trim();
1626
+ if (value) return value;
1627
+ }
1628
+ return void 0;
1629
+ }
1630
+ function readRequiredEnv(env, names, description) {
1631
+ const value = readEnv(env, names);
1632
+ if (value) return value;
1633
+ throw new Error(`Missing ${description}. Set one of: ${names.map((name) => `"${name}"`).join(`, `)}`);
1634
+ }
1635
+ function readPort(env) {
1636
+ const raw = readEnv(env, [`ELECTRIC_AGENTS_BUILTIN_PORT`, `PORT`]);
1637
+ if (!raw) return DEFAULT_PORT;
1638
+ const port = Number(raw);
1639
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid builtin agents port "${raw}". Expected an integer between 1 and 65535.`);
1640
+ return port;
1641
+ }
1642
+ function validateUrl(name, value) {
1643
+ try {
1644
+ new URL(value);
1645
+ return value;
1646
+ } catch {
1647
+ throw new Error(`Invalid ${name}: "${value}"`);
1648
+ }
1649
+ }
1650
+ function resolveBuiltinAgentsEntrypointOptions(env = process.env, cwd = process.cwd()) {
1651
+ const agentServerUrl = validateUrl(`agent server URL`, readRequiredEnv(env, [`ELECTRIC_AGENTS_SERVER_URL`, `ELECTRIC_AGENTS_BASE_URL`], `agent server base URL`));
1652
+ const baseUrl = readEnv(env, [`ELECTRIC_AGENTS_BUILTIN_BASE_URL`, `BUILTIN_AGENTS_BASE_URL`]);
1653
+ return {
1654
+ agentServerUrl,
1655
+ baseUrl: baseUrl ? validateUrl(`builtin agents base URL`, baseUrl) : void 0,
1656
+ host: readEnv(env, [`ELECTRIC_AGENTS_BUILTIN_HOST`, `HOST`]) ?? DEFAULT_HOST,
1657
+ port: readPort(env),
1658
+ workingDirectory: readEnv(env, [`ELECTRIC_AGENTS_WORKING_DIRECTORY`, `WORKING_DIRECTORY`]) ?? cwd
1659
+ };
1660
+ }
1661
+ async function runBuiltinAgentsEntrypoint({ env = process.env, cwd = process.cwd(), createServer: createServer$1 = (options$1) => new BuiltinAgentsServer(options$1) } = {}) {
1662
+ const options = resolveBuiltinAgentsEntrypointOptions(env, cwd);
1663
+ const server = createServer$1(options);
1664
+ const url = await server.start();
1665
+ return {
1666
+ options,
1667
+ server,
1668
+ url
1669
+ };
1670
+ }
1671
+
1672
+ //#endregion
1673
+ export { BuiltinAgentsServer, DEFAULT_BUILTIN_AGENT_HANDLER_PATH, HORTON_MODEL, WORKER_TOOL_NAMES, buildHortonSystemPrompt, createAgentHandler, createBuiltinAgentHandler, createHortonDocsSupport, createHortonTools, createSpawnWorkerTool, generateTitle, registerAgentTypes, registerBuiltinAgentTypes, registerHorton, registerWorker, resolveBuiltinAgentsEntrypointOptions, runBuiltinAgentsEntrypoint };