@electric-ax/agents 0.2.4 → 0.3.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.cjs CHANGED
@@ -27,18 +27,17 @@ const node_url = __toESM(require("node:url"));
27
27
  const __electric_ax_agents_runtime = __toESM(require("@electric-ax/agents-runtime"));
28
28
  const node_fs = __toESM(require("node:fs"));
29
29
  const pino = __toESM(require("pino"));
30
- const node_child_process = __toESM(require("node:child_process"));
31
- const node_os = __toESM(require("node:os"));
30
+ const __durable_streams_state = __toESM(require("@durable-streams/state"));
32
31
  const zod = __toESM(require("zod"));
33
- const agent_session_protocol = __toESM(require("agent-session-protocol"));
34
- const __anthropic_ai_sdk = __toESM(require("@anthropic-ai/sdk"));
35
32
  const node_crypto = __toESM(require("node:crypto"));
36
33
  const node_fs_promises = __toESM(require("node:fs/promises"));
37
34
  const better_sqlite3 = __toESM(require("better-sqlite3"));
38
35
  const __sinclair_typebox = __toESM(require("@sinclair/typebox"));
39
36
  const sqlite_vec = __toESM(require("sqlite-vec"));
40
37
  const nanoid = __toESM(require("nanoid"));
38
+ const __mariozechner_pi_ai = __toESM(require("@mariozechner/pi-ai"));
41
39
  const __electric_ax_agents_runtime_tools = __toESM(require("@electric-ax/agents-runtime/tools"));
40
+ const __electric_ax_agents_mcp = __toESM(require("@electric-ax/agents-mcp"));
42
41
  const node_http = __toESM(require("node:http"));
43
42
 
44
43
  //#region src/log.ts
@@ -94,516 +93,6 @@ const serverLog = {
94
93
  }
95
94
  };
96
95
 
97
- //#endregion
98
- //#region src/agents/coding-session.ts
99
- const defaultCliRunner = { async run(opts) {
100
- return new Promise((resolve, reject) => {
101
- const isClaude = opts.agent === `claude`;
102
- const bin = isClaude ? `claude` : `codex`;
103
- const args = isClaude ? opts.sessionId ? [
104
- `-r`,
105
- opts.sessionId,
106
- `--dangerously-skip-permissions`,
107
- `-p`
108
- ] : [`--dangerously-skip-permissions`, `-p`] : opts.sessionId ? [
109
- `exec`,
110
- `--skip-git-repo-check`,
111
- `resume`,
112
- opts.sessionId,
113
- opts.prompt
114
- ] : [
115
- `exec`,
116
- `--skip-git-repo-check`,
117
- opts.prompt
118
- ];
119
- const child = (0, node_child_process.spawn)(bin, args, {
120
- cwd: opts.cwd,
121
- stdio: [
122
- isClaude ? `pipe` : `ignore`,
123
- `pipe`,
124
- `pipe`
125
- ]
126
- });
127
- const MAX_BUF_CHARS = 4096;
128
- let stdout = ``;
129
- let stderr = ``;
130
- child.stdout?.on(`data`, (d) => {
131
- if (stdout.length < MAX_BUF_CHARS) stdout += d.toString().slice(0, MAX_BUF_CHARS - stdout.length);
132
- });
133
- child.stderr?.on(`data`, (d) => {
134
- if (stderr.length < MAX_BUF_CHARS) stderr += d.toString().slice(0, MAX_BUF_CHARS - stderr.length);
135
- });
136
- child.on(`error`, reject);
137
- child.on(`exit`, (code) => {
138
- resolve({
139
- exitCode: code ?? -1,
140
- stdout,
141
- stderr
142
- });
143
- });
144
- if (isClaude && child.stdin) {
145
- child.stdin.write(opts.prompt);
146
- child.stdin.end();
147
- }
148
- });
149
- } };
150
- async function discoverNewestSession(agent, cwd, excludeIds) {
151
- const all = await (0, agent_session_protocol.discoverSessions)(agent);
152
- const candidates = all.filter((s) => !excludeIds.has(s.sessionId) && (!s.cwd || s.cwd === cwd));
153
- if (candidates.length === 0) return null;
154
- return candidates[0].sessionId;
155
- }
156
- /**
157
- * Compute the candidate directories where Claude Code stores per-cwd
158
- * session JSONL files. Claude resolves the cwd to its realpath when
159
- * choosing the directory name (so /tmp/foo on macOS lands under
160
- * `-private-tmp-foo`), but the entity may have been spawned with the
161
- * non-realpath form. Return both candidates so the caller can union
162
- * their contents.
163
- */
164
- async function getClaudeProjectDirs(cwd) {
165
- const home = (0, node_os.homedir)();
166
- const make = (c) => node_path.default.join(home, `.claude`, `projects`, c.replace(/\//g, `-`));
167
- const dirs = [make(cwd)];
168
- try {
169
- const real = await node_fs.promises.realpath(cwd);
170
- if (real !== cwd) dirs.push(make(real));
171
- } catch {}
172
- return dirs;
173
- }
174
- async function listClaudeJsonlIdsByCwd(cwd) {
175
- const ids = new Set();
176
- for (const dir of await getClaudeProjectDirs(cwd)) try {
177
- const files = await node_fs.promises.readdir(dir);
178
- for (const f of files) if (f.endsWith(`.jsonl`)) ids.add(f.slice(0, -`.jsonl`.length));
179
- } catch {}
180
- return ids;
181
- }
182
- /**
183
- * Deterministic-path discovery for a freshly created session. After the
184
- * Claude CLI runs in `-p` mode it writes the new JSONL straight into
185
- * `~/.claude/projects/<sanitize(cwd)>/<id>.jsonl` *without* leaving a
186
- * `~/.claude/sessions/<pid>.json` lock file (those are interactive-only),
187
- * so `discoverSessions` can miss it. Compute the expected dir directly
188
- * and diff its contents against a pre-run snapshot. Returns the newest
189
- * fresh sessionId or null. Codex falls back to discoverNewestSession.
190
- */
191
- async function findNewSessionAfterRun(agent, cwd, preDirectIds, preDiscoveredIds) {
192
- if (agent === `claude`) {
193
- const dirs = await getClaudeProjectDirs(cwd);
194
- let best = null;
195
- for (const dir of dirs) try {
196
- const files = await node_fs.promises.readdir(dir);
197
- for (const f of files) {
198
- if (!f.endsWith(`.jsonl`)) continue;
199
- const id = f.slice(0, -`.jsonl`.length);
200
- if (preDirectIds.has(id)) continue;
201
- const st = await node_fs.promises.stat(node_path.default.join(dir, f)).catch(() => null);
202
- if (!st) continue;
203
- if (!best || st.mtimeMs > best.mtime) best = {
204
- id,
205
- mtime: st.mtimeMs
206
- };
207
- }
208
- } catch {}
209
- if (best) return best.id;
210
- }
211
- return discoverNewestSession(agent, cwd, preDiscoveredIds);
212
- }
213
- const sessionMetaRowSchema = zod.z.object({
214
- key: zod.z.literal(`current`),
215
- electricSessionId: zod.z.string(),
216
- nativeSessionId: zod.z.string().optional(),
217
- agent: zod.z.enum([`claude`, `codex`]),
218
- cwd: zod.z.string(),
219
- status: zod.z.enum([
220
- `initializing`,
221
- `idle`,
222
- `running`,
223
- `error`
224
- ]),
225
- error: zod.z.string().optional(),
226
- currentPromptInboxKey: zod.z.string().optional()
227
- });
228
- const cursorStateRowSchema = zod.z.object({
229
- key: zod.z.literal(`current`),
230
- cursor: zod.z.string(),
231
- lastProcessedInboxKey: zod.z.string().optional()
232
- });
233
- const eventRowSchema = zod.z.object({
234
- key: zod.z.string(),
235
- ts: zod.z.number(),
236
- type: zod.z.string(),
237
- callId: zod.z.string().optional(),
238
- payload: zod.z.looseObject({})
239
- });
240
- const creationArgsSchema = zod.z.object({
241
- agent: zod.z.enum([`claude`, `codex`]),
242
- cwd: zod.z.string().optional(),
243
- nativeSessionId: zod.z.string().optional(),
244
- importFrom: zod.z.object({
245
- agent: zod.z.enum([`claude`, `codex`]),
246
- sessionId: zod.z.string()
247
- }).optional()
248
- });
249
- const promptMessageSchema = zod.z.object({ text: zod.z.string() });
250
- /**
251
- * Stable key for an events-collection row, derived from the event's content.
252
- * Lets us re-insert the same event without producing duplicates — the caller
253
- * (or the collection's uniqueness guard) uses this to de-dup across retries,
254
- * replays, and crash recovery. Sorts chronologically by ts, then by type.
255
- */
256
- function eventKey(event) {
257
- const tsPart = String(event.ts).padStart(16, `0`);
258
- return `${tsPart}_${event.type}_${contentHashHex(event)}`;
259
- }
260
- function contentHashHex(event) {
261
- const json = JSON.stringify(event);
262
- let h = 5381;
263
- for (let i = 0; i < json.length; i++) h = (h * 33 ^ json.charCodeAt(i)) >>> 0;
264
- return h.toString(16).padStart(8, `0`);
265
- }
266
- function buildEventRow(event) {
267
- const callId = `callId` in event && typeof event.callId === `string` ? event.callId : void 0;
268
- return {
269
- key: eventKey(event),
270
- ts: event.ts,
271
- type: event.type,
272
- ...callId !== void 0 ? { callId } : {},
273
- payload: event
274
- };
275
- }
276
- function appendIfNew(ctx, event) {
277
- const row = buildEventRow(event);
278
- if (ctx.events.get(row.key) !== void 0) return;
279
- ctx.actions.events_insert({ row });
280
- }
281
- /**
282
- * Mirror every event that lands in the JSONL file while `runWork` is
283
- * executing (i.e. while the CLI is running). Returns the advanced cursor
284
- * and the `runWork` result once everything has settled and every append
285
- * has been persisted to the entity's durable stream.
286
- *
287
- * If setup fails (e.g. the session file can't be resolved), `runWork`
288
- * still runs — but nothing is mirrored and `setupError` is populated so
289
- * the caller can surface the condition. If `runWork` throws, the error
290
- * propagates after the watcher has been cleaned up.
291
- */
292
- async function runWithLiveMirror(opts) {
293
- let cursor = null;
294
- let setupError = void 0;
295
- try {
296
- const session = await (0, agent_session_protocol.resolveSession)(opts.nativeSessionId, opts.agent);
297
- if (opts.serializedCursor) cursor = (0, agent_session_protocol.deserializeCursor)({
298
- ...opts.serializedCursor,
299
- path: session.path
300
- });
301
- else {
302
- const initial = await (0, agent_session_protocol.loadSession)({
303
- sessionId: opts.nativeSessionId,
304
- agent: opts.agent
305
- });
306
- for (const ev of initial.events) appendIfNew(opts.ctx, ev);
307
- cursor = initial.cursor;
308
- }
309
- } catch (e) {
310
- setupError = e;
311
- }
312
- if (!cursor) {
313
- const result$1 = await opts.runWork();
314
- return {
315
- cursor: opts.serializedCursor,
316
- setupError,
317
- result: result$1
318
- };
319
- }
320
- let activeCursor = cursor;
321
- let busy = false;
322
- let pending = false;
323
- let stopped = false;
324
- const drainOnce = async () => {
325
- if (stopped && busy) return;
326
- if (busy) {
327
- pending = true;
328
- return;
329
- }
330
- busy = true;
331
- try {
332
- const res = await (0, agent_session_protocol.tailSession)({ cursor: activeCursor });
333
- activeCursor = res.cursor;
334
- for (const ev of res.newEvents) appendIfNew(opts.ctx, ev);
335
- } catch {} finally {
336
- busy = false;
337
- if (pending && !stopped) {
338
- pending = false;
339
- drainOnce();
340
- }
341
- }
342
- };
343
- const fileWatcher = (0, node_fs.watch)(activeCursor.path, () => {
344
- drainOnce();
345
- });
346
- const pollHandle = setInterval(() => {
347
- drainOnce();
348
- }, 1500);
349
- let result;
350
- try {
351
- result = await opts.runWork();
352
- } finally {
353
- stopped = true;
354
- clearInterval(pollHandle);
355
- fileWatcher.close();
356
- while (busy) await new Promise((r) => setTimeout(r, 10));
357
- try {
358
- const final = await (0, agent_session_protocol.tailSession)({ cursor: activeCursor });
359
- activeCursor = final.cursor;
360
- for (const ev of final.newEvents) appendIfNew(opts.ctx, ev);
361
- } catch {}
362
- }
363
- return {
364
- cursor: (0, agent_session_protocol.serializeCursor)(activeCursor),
365
- setupError,
366
- result
367
- };
368
- }
369
- function registerCodingSession(registry, options = {}) {
370
- const runner = options.cliRunner ?? defaultCliRunner;
371
- const defaultCwd = options.defaultWorkingDirectory ?? process.cwd();
372
- registry.define(`coder`, {
373
- description: `Runs a Claude Code / Codex CLI session and mirrors its normalized event stream into a durable store. Prompts arrive via message_received (type: "prompt") and are executed serially.`,
374
- creationSchema: creationArgsSchema,
375
- inboxSchemas: { prompt: promptMessageSchema },
376
- state: {
377
- sessionMeta: {
378
- schema: sessionMetaRowSchema,
379
- type: __electric_ax_agents_runtime.CODING_SESSION_META_COLLECTION_TYPE,
380
- primaryKey: `key`
381
- },
382
- cursorState: {
383
- schema: cursorStateRowSchema,
384
- type: __electric_ax_agents_runtime.CODING_SESSION_CURSOR_COLLECTION_TYPE,
385
- primaryKey: `key`
386
- },
387
- events: {
388
- schema: eventRowSchema,
389
- type: __electric_ax_agents_runtime.CODING_SESSION_EVENT_COLLECTION_TYPE,
390
- primaryKey: `key`
391
- }
392
- },
393
- async handler(ctx, _wake) {
394
- const existingMeta = ctx.db.collections.sessionMeta.get(`current`);
395
- if (!existingMeta) {
396
- const args = creationArgsSchema.parse(ctx.args);
397
- const cwd = args.cwd ?? defaultCwd;
398
- const electricSessionId = ctx.entityUrl.split(`/`).pop() ?? ctx.entityUrl;
399
- let resolvedNativeId = args.nativeSessionId;
400
- if (args.importFrom) {
401
- const result = await (0, agent_session_protocol.importLocalSession)({
402
- source: {
403
- sessionId: args.importFrom.sessionId,
404
- agent: args.importFrom.agent
405
- },
406
- target: {
407
- agent: args.agent,
408
- cwd
409
- }
410
- });
411
- resolvedNativeId = result.sessionId;
412
- }
413
- const hasNative = resolvedNativeId !== void 0;
414
- ctx.db.actions.sessionMeta_insert({ row: {
415
- key: `current`,
416
- electricSessionId,
417
- ...hasNative ? { nativeSessionId: resolvedNativeId } : {},
418
- agent: args.agent,
419
- cwd,
420
- status: hasNative ? `idle` : `initializing`
421
- } });
422
- }
423
- if (!ctx.db.collections.cursorState.get(`current`)) ctx.db.actions.cursorState_insert({ row: {
424
- key: `current`,
425
- cursor: ``
426
- } });
427
- const metaRow = ctx.db.collections.sessionMeta.get(`current`);
428
- const cursorRow = ctx.db.collections.cursorState.get(`current`);
429
- if (!metaRow || !cursorRow) throw new Error(`[coding-session] expected sessionMeta and cursorState rows to exist after init`);
430
- if (metaRow.nativeSessionId && !cursorRow.cursor) {
431
- const mirrorCtx = {
432
- events: { get: (k) => ctx.db.collections.events.get(k) },
433
- actions: { events_insert: ctx.db.actions.events_insert }
434
- };
435
- try {
436
- const initial = await (0, agent_session_protocol.loadSession)({
437
- sessionId: metaRow.nativeSessionId,
438
- agent: metaRow.agent
439
- });
440
- for (const ev of initial.events) appendIfNew(mirrorCtx, ev);
441
- const serialized = (0, agent_session_protocol.serializeCursor)(initial.cursor);
442
- ctx.db.actions.cursorState_update({
443
- key: `current`,
444
- updater: (d) => {
445
- d.cursor = JSON.stringify(serialized);
446
- }
447
- });
448
- } catch (e) {
449
- const message = e instanceof Error ? e.message : String(e);
450
- ctx.db.actions.sessionMeta_update({
451
- key: `current`,
452
- updater: (d) => {
453
- d.error = `initial mirror failed: ${message}`;
454
- }
455
- });
456
- }
457
- }
458
- const inboxRows = ctx.db.collections.inbox.toArray.slice().sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
459
- const lastKey = cursorRow.lastProcessedInboxKey ?? ``;
460
- const pending = inboxRows.filter((m) => m.key > lastKey);
461
- if (pending.length === 0) {
462
- if (metaRow.status === `running` || metaRow.status === `error`) ctx.db.actions.sessionMeta_update({
463
- key: `current`,
464
- updater: (d) => {
465
- d.status = `idle`;
466
- delete d.currentPromptInboxKey;
467
- delete d.error;
468
- }
469
- });
470
- return;
471
- }
472
- let runningMeta = metaRow;
473
- let runningCursor = cursorRow;
474
- for (const inboxMsg of pending) {
475
- const parsed = promptMessageSchema.safeParse(inboxMsg.payload);
476
- if (!parsed.success) {
477
- ctx.db.actions.cursorState_update({
478
- key: `current`,
479
- updater: (d) => {
480
- d.lastProcessedInboxKey = inboxMsg.key;
481
- }
482
- });
483
- runningCursor = {
484
- ...runningCursor,
485
- lastProcessedInboxKey: inboxMsg.key
486
- };
487
- continue;
488
- }
489
- const prompt = parsed.data.text;
490
- const existingTitle = ctx.tags.title;
491
- if (typeof existingTitle !== `string` || existingTitle.length === 0) ctx.setTag(`title`, prompt.slice(0, 80));
492
- ctx.db.actions.sessionMeta_update({
493
- key: `current`,
494
- updater: (d) => {
495
- d.status = `running`;
496
- d.currentPromptInboxKey = inboxMsg.key;
497
- delete d.error;
498
- }
499
- });
500
- const recordedRun = ctx.recordRun();
501
- const eventKeysBefore = new Set(ctx.db.collections.events.toArray.map((e) => e.key));
502
- try {
503
- const mirrorCtx = {
504
- events: { get: (k) => ctx.db.collections.events.get(k) },
505
- actions: { events_insert: ctx.db.actions.events_insert }
506
- };
507
- let nextCursorJson = runningCursor.cursor;
508
- if (!runningMeta.nativeSessionId) {
509
- const preDirectIds = runningMeta.agent === `claude` ? await listClaudeJsonlIdsByCwd(runningMeta.cwd) : new Set();
510
- const preDiscoveredIds = new Set((await (0, agent_session_protocol.discoverSessions)(runningMeta.agent)).map((s) => s.sessionId));
511
- const cliResult = await runner.run({
512
- agent: runningMeta.agent,
513
- cwd: runningMeta.cwd,
514
- prompt
515
- });
516
- if (cliResult.exitCode !== 0) throw new Error(`[coding-session] ${runningMeta.agent} CLI exited ${cliResult.exitCode}. stderr=${cliResult.stderr.slice(0, 800) || `<empty>`} stdout=${cliResult.stdout.slice(0, 800) || `<empty>`}`);
517
- const foundId = await findNewSessionAfterRun(runningMeta.agent, runningMeta.cwd, preDirectIds, preDiscoveredIds);
518
- if (!foundId) throw new Error(`[coding-session] ${runningMeta.agent} CLI succeeded but no new session file was found`);
519
- ctx.db.actions.sessionMeta_update({
520
- key: `current`,
521
- updater: (d) => {
522
- d.nativeSessionId = foundId;
523
- }
524
- });
525
- runningMeta = {
526
- ...runningMeta,
527
- nativeSessionId: foundId
528
- };
529
- const initial = await (0, agent_session_protocol.loadSession)({
530
- sessionId: foundId,
531
- agent: runningMeta.agent
532
- });
533
- for (const ev of initial.events) appendIfNew(mirrorCtx, ev);
534
- nextCursorJson = JSON.stringify((0, agent_session_protocol.serializeCursor)(initial.cursor));
535
- } else {
536
- const serializedCursor = runningCursor.cursor ? JSON.parse(runningCursor.cursor) : null;
537
- const { cursor: nextSerialized, setupError, result: cliResult } = await runWithLiveMirror({
538
- agent: runningMeta.agent,
539
- nativeSessionId: runningMeta.nativeSessionId,
540
- serializedCursor,
541
- ctx: mirrorCtx,
542
- runWork: () => runner.run({
543
- agent: runningMeta.agent,
544
- sessionId: runningMeta.nativeSessionId,
545
- cwd: runningMeta.cwd,
546
- prompt
547
- })
548
- });
549
- if (setupError) throw setupError instanceof Error ? setupError : new Error(String(setupError));
550
- if (cliResult.exitCode !== 0) throw new Error(`[coding-session] ${runningMeta.agent} CLI exited ${cliResult.exitCode}. stderr=${cliResult.stderr.slice(0, 800) || `<empty>`} stdout=${cliResult.stdout.slice(0, 800) || `<empty>`}`);
551
- const persistedCursor = nextSerialized ?? serializedCursor;
552
- nextCursorJson = persistedCursor ? JSON.stringify(persistedCursor) : ``;
553
- }
554
- ctx.db.actions.cursorState_update({
555
- key: `current`,
556
- updater: (d) => {
557
- d.cursor = nextCursorJson;
558
- d.lastProcessedInboxKey = inboxMsg.key;
559
- }
560
- });
561
- runningCursor = {
562
- ...runningCursor,
563
- cursor: nextCursorJson,
564
- lastProcessedInboxKey: inboxMsg.key
565
- };
566
- for (const row of ctx.db.collections.events.toArray) {
567
- if (eventKeysBefore.has(row.key)) continue;
568
- if (row.type !== `assistant_message`) continue;
569
- const text = row.payload?.text;
570
- if (typeof text === `string` && text.length > 0) recordedRun.attachResponse(text);
571
- }
572
- recordedRun.end({ status: `completed` });
573
- } catch (e) {
574
- const message = e instanceof Error ? e.message : String(e);
575
- recordedRun.end({
576
- status: `failed`,
577
- finishReason: `error`
578
- });
579
- ctx.db.actions.sessionMeta_update({
580
- key: `current`,
581
- updater: (d) => {
582
- d.status = `error`;
583
- d.error = message;
584
- }
585
- });
586
- ctx.db.actions.cursorState_update({
587
- key: `current`,
588
- updater: (d) => {
589
- d.lastProcessedInboxKey = inboxMsg.key;
590
- }
591
- });
592
- throw e;
593
- }
594
- }
595
- ctx.db.actions.sessionMeta_update({
596
- key: `current`,
597
- updater: (d) => {
598
- d.status = `idle`;
599
- delete d.currentPromptInboxKey;
600
- delete d.error;
601
- }
602
- });
603
- }
604
- });
605
- }
606
-
607
96
  //#endregion
608
97
  //#region src/docs/embed.ts
609
98
  const EMBEDDING_DIMENSIONS = 128;
@@ -1408,11 +897,11 @@ const WORKER_TOOL_NAMES = [
1408
897
  `read`,
1409
898
  `write`,
1410
899
  `edit`,
1411
- `brave_search`,
900
+ `web_search`,
1412
901
  `fetch_url`,
1413
902
  `spawn_worker`
1414
903
  ];
1415
- function createSpawnWorkerTool(ctx) {
904
+ function createSpawnWorkerTool(ctx, modelConfig) {
1416
905
  return {
1417
906
  name: `spawn_worker`,
1418
907
  label: `Spawn Worker`,
@@ -1439,10 +928,16 @@ function createSpawnWorkerTool(ctx) {
1439
928
  details: { spawned: false }
1440
929
  };
1441
930
  const id = (0, nanoid.nanoid)(10);
931
+ const workerModelArgs = modelConfig ? {
932
+ provider: modelConfig.provider,
933
+ model: modelConfig.model,
934
+ ...modelConfig.reasoningEffort && { reasoningEffort: modelConfig.reasoningEffort }
935
+ } : {};
1442
936
  try {
1443
937
  const handle = await ctx.spawn(`worker`, id, {
1444
938
  systemPrompt,
1445
- tools
939
+ tools,
940
+ ...workerModelArgs
1446
941
  }, {
1447
942
  initialMessage,
1448
943
  wake: {
@@ -1476,140 +971,138 @@ function createSpawnWorkerTool(ctx) {
1476
971
  }
1477
972
 
1478
973
  //#endregion
1479
- //#region src/tools/spawn-coder.ts
1480
- const CODER_AGENT_NAMES = [`claude`, `codex`];
1481
- function createSpawnCoderTool(ctx) {
974
+ //#region src/model-catalog.ts
975
+ const REASONING_EFFORT_VALUES = [
976
+ `auto`,
977
+ `minimal`,
978
+ `low`,
979
+ `medium`,
980
+ `high`
981
+ ];
982
+ const DEFAULT_ANTHROPIC_MODEL = `claude-sonnet-4-6`;
983
+ const DEFAULT_OPENAI_MODEL = `gpt-4.1`;
984
+ const DEFAULT_CODEX_MODEL = `gpt-5.4`;
985
+ function modelValue(provider, id) {
986
+ return `${provider}:${id}`;
987
+ }
988
+ function providerLabel(provider) {
989
+ if (provider === `anthropic`) return `Anthropic`;
990
+ if (provider === `openai-codex`) return `OpenAI Codex`;
991
+ return `OpenAI`;
992
+ }
993
+ function configuredProviders() {
994
+ return (0, __electric_ax_agents_runtime.detectAvailableProviders)();
995
+ }
996
+ function mockFallbackCatalog() {
997
+ const fallback = {
998
+ provider: `anthropic`,
999
+ id: DEFAULT_ANTHROPIC_MODEL,
1000
+ label: `Anthropic ${DEFAULT_ANTHROPIC_MODEL}`,
1001
+ value: modelValue(`anthropic`, DEFAULT_ANTHROPIC_MODEL),
1002
+ reasoning: true
1003
+ };
1482
1004
  return {
1483
- name: `spawn_coder`,
1484
- label: `Spawn Coder`,
1485
- description: `Spawn a coding-session subagent (a coder) that drives a Claude Code or Codex CLI session in a working directory. Use when the user asks for code changes, file edits, debugging, or any task that benefits from a real coding agent with tool access. The coder is long-lived — its URL stays valid across many turns, so you can keep prompting it via prompt_coder without re-spawning. End your turn after spawning; you'll be woken when the coder finishes its first reply.`,
1486
- parameters: __sinclair_typebox.Type.Object({
1487
- prompt: __sinclair_typebox.Type.String({ description: `First user message sent to the coder. This is what kicks off the run — without it the coder will idle. Be concrete: describe the task, mention the files/paths involved, and what form of answer you want back.` }),
1488
- agent: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union(CODER_AGENT_NAMES.map((n) => __sinclair_typebox.Type.Literal(n)), { description: `Which coding agent to use. Defaults to "claude". Use "codex" only if the user explicitly asks for it.` })),
1489
- cwd: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String({ description: `Working directory the coder runs in. Defaults to the runtime's cwd (the same directory Horton is running in). Set this when the user wants the coder to operate on a different repo.` }))
1490
- }),
1491
- execute: async (_toolCallId, params) => {
1492
- const { prompt, agent, cwd } = params;
1493
- if (typeof prompt !== `string` || prompt.length === 0) return {
1494
- content: [{
1495
- type: `text`,
1496
- text: `Error: prompt is required and must be a non-empty string.`
1497
- }],
1498
- details: { spawned: false }
1499
- };
1500
- const id = (0, nanoid.nanoid)(10);
1501
- const spawnArgs = { agent: agent ?? `claude` };
1502
- if (cwd) spawnArgs.cwd = cwd;
1503
- try {
1504
- const handle = await ctx.spawn(`coder`, id, spawnArgs, {
1505
- initialMessage: { text: prompt },
1506
- wake: {
1507
- on: `runFinished`,
1508
- includeResponse: true
1509
- }
1510
- });
1511
- const coderUrl = handle.entityUrl;
1512
- return {
1513
- content: [{
1514
- type: `text`,
1515
- text: `Coder dispatched at ${coderUrl}. End your turn — when the coder finishes its current reply you'll be woken with the response. To send follow-up prompts to the same coder, call prompt_coder with this URL.`
1516
- }],
1517
- details: {
1518
- spawned: true,
1519
- coderUrl
1520
- }
1521
- };
1522
- } catch (err) {
1523
- serverLog.warn(`[spawn_coder tool] failed to spawn coder ${id}: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
1524
- return {
1525
- content: [{
1526
- type: `text`,
1527
- text: `Error spawning coder: ${err instanceof Error ? err.message : `Unknown error`}`
1528
- }],
1529
- details: { spawned: false }
1530
- };
1531
- }
1532
- }
1005
+ choices: [fallback],
1006
+ defaultChoice: fallback
1533
1007
  };
1534
1008
  }
1535
- function createPromptCoderTool(ctx) {
1009
+ async function fetchAvailableModelIds(provider) {
1010
+ try {
1011
+ const res = provider === `anthropic` ? await fetch(`https://api.anthropic.com/v1/models`, {
1012
+ headers: {
1013
+ "x-api-key": process.env.ANTHROPIC_API_KEY ?? ``,
1014
+ "anthropic-version": `2023-06-01`
1015
+ },
1016
+ signal: AbortSignal.timeout(3e3)
1017
+ }) : await fetch(`https://api.openai.com/v1/models`, {
1018
+ headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ``}` },
1019
+ signal: AbortSignal.timeout(3e3)
1020
+ });
1021
+ if (res.status === 401 || res.status === 403) return new Set();
1022
+ if (!res.ok) return null;
1023
+ const body = await res.json();
1024
+ const ids = new Set((body.data ?? []).map((model) => model.id).filter((id) => typeof id === `string`));
1025
+ return ids.size > 0 ? ids : null;
1026
+ } catch {
1027
+ return null;
1028
+ }
1029
+ }
1030
+ async function choicesForProvider(provider) {
1031
+ const knownModels = (0, __mariozechner_pi_ai.getModels)(provider);
1032
+ if (provider === `openai-codex`) return knownModels.map((model) => ({
1033
+ provider,
1034
+ id: model.id,
1035
+ label: `${providerLabel(provider)} ${model.name}`,
1036
+ value: modelValue(provider, model.id),
1037
+ reasoning: model.reasoning
1038
+ }));
1039
+ const availableIds = await fetchAvailableModelIds(provider);
1040
+ const models = availableIds === null ? knownModels : knownModels.filter((model) => availableIds.has(model.id));
1041
+ return models.map((model) => ({
1042
+ provider,
1043
+ id: model.id,
1044
+ label: `${providerLabel(provider)} ${model.name}`,
1045
+ value: modelValue(provider, model.id),
1046
+ reasoning: model.reasoning
1047
+ }));
1048
+ }
1049
+ function withProviderPayloadDefaults(config, choice, reasoningEffort) {
1050
+ if (choice.provider !== `openai` && choice.provider !== `openai-codex` || !choice.reasoning) return config;
1051
+ const defaultEffort = choice.provider === `openai-codex` ? `low` : `minimal`;
1052
+ const effort = reasoningEffort === `minimal` && choice.provider === `openai-codex` ? `low` : reasoningEffort ?? defaultEffort;
1536
1053
  return {
1537
- name: `prompt_coder`,
1538
- label: `Prompt Coder`,
1539
- description: `Send a follow-up prompt to a coder you previously spawned. The prompt is queued on the coder's inbox and runs as the next CLI turn. End your turn after calling — you'll be woken when the coder's reply lands.`,
1540
- parameters: __sinclair_typebox.Type.Object({
1541
- coder_url: __sinclair_typebox.Type.String({ description: `Entity URL returned by spawn_coder, e.g. "/coder/abc123". Must be the URL of a coder you previously spawned in this conversation.` }),
1542
- prompt: __sinclair_typebox.Type.String({ description: `Follow-up message to send to the coder. Treat this like the next turn in a chat — reference earlier context the coder already saw rather than restating it.` })
1543
- }),
1544
- execute: async (_toolCallId, params) => {
1545
- const { coder_url, prompt } = params;
1546
- if (typeof coder_url !== `string` || !coder_url.startsWith(`/coder/`)) return {
1547
- content: [{
1548
- type: `text`,
1549
- text: `Error: coder_url must be a path like "/coder/<id>".`
1550
- }],
1551
- details: { sent: false }
1552
- };
1553
- if (typeof prompt !== `string` || prompt.length === 0) return {
1554
- content: [{
1555
- type: `text`,
1556
- text: `Error: prompt is required and must be a non-empty string.`
1557
- }],
1558
- details: { sent: false }
1054
+ ...config,
1055
+ onPayload: (payload) => {
1056
+ if (typeof payload !== `object` || payload === null) return void 0;
1057
+ const body = payload;
1058
+ const existingReasoning = typeof body.reasoning === `object` && body.reasoning !== null ? body.reasoning : {};
1059
+ return {
1060
+ ...body,
1061
+ reasoning: {
1062
+ ...existingReasoning,
1063
+ effort
1064
+ }
1559
1065
  };
1560
- try {
1561
- ctx.send(coder_url, { text: prompt });
1562
- return {
1563
- content: [{
1564
- type: `text`,
1565
- text: `Prompt queued for ${coder_url}. End your turn — you'll be woken when the coder's reply lands.`
1566
- }],
1567
- details: {
1568
- sent: true,
1569
- coderUrl: coder_url
1570
- }
1571
- };
1572
- } catch (err) {
1573
- serverLog.warn(`[prompt_coder tool] failed to send to ${coder_url}: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
1574
- return {
1575
- content: [{
1576
- type: `text`,
1577
- text: `Error sending prompt to coder: ${err instanceof Error ? err.message : `Unknown error`}`
1578
- }],
1579
- details: { sent: false }
1580
- };
1581
- }
1582
1066
  }
1583
1067
  };
1584
1068
  }
1069
+ function parseReasoningEffort(value) {
1070
+ return value === `minimal` || value === `low` || value === `medium` || value === `high` ? value : null;
1071
+ }
1072
+ async function createBuiltinModelCatalog(options = {}) {
1073
+ const providers = configuredProviders();
1074
+ if (providers.length === 0 && options.allowMockFallback) return mockFallbackCatalog();
1075
+ const choices = (await Promise.all(providers.map((provider) => choicesForProvider(provider)))).flat();
1076
+ if (choices.length === 0) return options.allowMockFallback ? mockFallbackCatalog() : null;
1077
+ const defaultChoice = choices.find((choice) => choice.provider === `anthropic` && choice.id === DEFAULT_ANTHROPIC_MODEL) ?? choices.find((choice) => choice.provider === `openai` && choice.id === DEFAULT_OPENAI_MODEL) ?? choices.find((choice) => choice.provider === `openai-codex` && choice.id === DEFAULT_CODEX_MODEL) ?? choices[0];
1078
+ return {
1079
+ choices,
1080
+ defaultChoice
1081
+ };
1082
+ }
1083
+ function resolveBuiltinModelConfig(catalog, args) {
1084
+ const modelArg = args.model;
1085
+ const providerArg = args.provider;
1086
+ const reasoningEffort = parseReasoningEffort(args.reasoningEffort);
1087
+ const selected = typeof modelArg === `string` ? catalog.choices.find((choice$1) => choice$1.value === modelArg || choice$1.id === modelArg && choice$1.provider === providerArg) : void 0;
1088
+ const choice = selected ?? catalog.defaultChoice;
1089
+ const config = {
1090
+ provider: choice.provider,
1091
+ model: choice.id,
1092
+ ...reasoningEffort && { reasoningEffort },
1093
+ ...choice.provider === `openai-codex` && { getApiKey: () => (0, __electric_ax_agents_runtime.readCodexAccessToken)() }
1094
+ };
1095
+ return withProviderPayloadDefaults(config, choice, reasoningEffort);
1096
+ }
1097
+ function modelChoiceValues(catalog) {
1098
+ return catalog.choices.map((choice) => choice.value);
1099
+ }
1585
1100
 
1586
1101
  //#endregion
1587
1102
  //#region src/agents/horton.ts
1588
- const TITLE_MODEL = `claude-haiku-4-5-20251001`;
1589
1103
  const HORTON_MODEL = `claude-sonnet-4-6`;
1590
- let anthropic = null;
1591
- function getClient() {
1592
- if (!anthropic) anthropic = new __anthropic_ai_sdk.default();
1593
- return anthropic;
1594
- }
1595
- async function defaultHaikuCall(prompt) {
1596
- const client = getClient();
1597
- const res = await client.messages.create({
1598
- model: TITLE_MODEL,
1599
- max_tokens: 64,
1600
- messages: [{
1601
- role: `user`,
1602
- content: prompt
1603
- }]
1604
- });
1605
- const block = res.content[0];
1606
- return block?.type === `text` ? block.text : ``;
1607
- }
1608
- const TITLE_PROMPT = (userMessage) => `Summarize the following user request in 3-5 words for use as a chat session title.
1609
- Respond with only the title, no quotes, no punctuation, no preamble.
1610
-
1611
- User request:
1612
- ${userMessage}`;
1104
+ const TITLE_SYSTEM_PROMPT = "You generate concise chat session titles in 3-5 words. Respond with only the title, no quotes, no punctuation, no preamble.";
1105
+ const TITLE_USER_PROMPT = (userMessage) => `User request:\n${userMessage}`;
1613
1106
  const TITLE_STOP_WORDS = new Set([
1614
1107
  `a`,
1615
1108
  `an`,
@@ -1677,19 +1170,34 @@ function buildFallbackTitle(userMessage) {
1677
1170
  const selected = informativeWords.length >= 2 ? informativeWords.slice(0, 5) : backupWords;
1678
1171
  return selected.join(` `).slice(0, 80).trim() || `Untitled Chat`;
1679
1172
  }
1680
- async function generateTitle(userMessage, llmCall = defaultHaikuCall) {
1173
+ function createConfiguredTitleCall(catalog, modelConfig, logPrefix) {
1174
+ return (prompt) => (0, __electric_ax_agents_runtime.completeWithLowCostModel)({
1175
+ catalog,
1176
+ modelConfig,
1177
+ log: (message) => serverLog.info(message),
1178
+ logPrefix,
1179
+ purpose: `title generation`,
1180
+ systemPrompt: TITLE_SYSTEM_PROMPT,
1181
+ prompt,
1182
+ maxTokens: 64
1183
+ });
1184
+ }
1185
+ async function generateTitle(userMessage, llmCall, onFallback) {
1681
1186
  try {
1682
- const raw = await llmCall(TITLE_PROMPT(userMessage));
1187
+ const raw = await llmCall(TITLE_USER_PROMPT(userMessage));
1683
1188
  const title = raw.trim();
1684
- return title.length > 0 ? title : buildFallbackTitle(userMessage);
1685
- } catch {
1189
+ if (title.length > 0) return title;
1190
+ onFallback?.(`empty LLM title response`);
1191
+ return buildFallbackTitle(userMessage);
1192
+ } catch (err) {
1193
+ onFallback?.(err instanceof Error ? err.message : String(err));
1686
1194
  return buildFallbackTitle(userMessage);
1687
1195
  }
1688
1196
  }
1689
1197
  function buildHortonSystemPrompt(workingDirectory, opts = {}) {
1690
1198
  const docsTools = opts.hasDocsSupport ? `\n- search_durable_agents_docs: hybrid search over the built-in Durable Agents docs index` : ``;
1691
1199
  const skillsTools = opts.hasSkills ? `\n- use_skill: load a skill (knowledge, instructions, or a tutorial) into your context to help with the user's request\n- remove_skill: unload a skill from context when you're done with it` : ``;
1692
- const docsGuidance = opts.hasDocsSupport ? `\n- For ANY question about Electric Agents, Durable Agents, or this framework, ALWAYS use search_durable_agents_docs FIRST. Do not use brave_search or fetch_url for Electric Agents topics unless the docs search returns no useful results.\n- The search tool returns chunk content directly — you do not need to read the source files.\n- Use repo read/bash tools only for non-doc files or when you need to inspect exact implementation code in the workspace.` : ``;
1200
+ const docsGuidance = opts.hasDocsSupport ? `\n- For ANY question about Electric Agents, Durable Agents, or this framework, ALWAYS use search_durable_agents_docs FIRST. Do not use web_search or fetch_url for Electric Agents topics unless the docs search returns no useful results.\n- The search tool returns chunk content directly — you do not need to read the source files.\n- Use repo read/bash tools only for non-doc files or when you need to inspect exact implementation code in the workspace.` : ``;
1693
1201
  const skillsGuidance = opts.hasSkills ? `\n# Skills\nYou have access to skills — specialized knowledge and guided workflows you can load on demand. Your context includes a skills catalog listing what's available. When the user's request matches a skill's description or keywords, load it with use_skill.
1694
1202
 
1695
1203
  Some skills are user-invocable — the user can trigger them with a slash command like \`/quickstart\`. When you see a message starting with \`/\` followed by a skill name, load that skill immediately with use_skill. Pass any text after the skill name as args.
@@ -1725,7 +1233,9 @@ Don't force onboarding. If someone just wants to chat or code, let them. When in
1725
1233
  - ${opts.hasDocsSupport ? `If search_durable_agents_docs is available, use it first (faster, hybrid search).` : `Use fetch_url to look up documentation pages.`}
1726
1234
  - The Electric Agents docs site is at ${opts.docsUrl}
1727
1235
  - The docs site covers: Usage (entity definition, handlers, tools, state, spawning, coordination, waking, shared state, client integration, app setup), Reference (handler context, entity definitions, configurations, tools, state proxies, wake events, registries), Entities (Horton, Worker), and Patterns (Manager-Worker, Pipeline, Map-Reduce, Dispatcher, Blackboard, Reactive Observers).
1728
- - For general coding questions unrelated to Electric Agents, use brave_search or your own knowledge.` : ``;
1236
+ - For general coding questions unrelated to Electric Agents, use web_search or your own knowledge.` : ``;
1237
+ const modelGuidance = opts.modelProvider && opts.modelId ? `\n# Runtime model
1238
+ You are currently running via provider "${opts.modelProvider}" with model "${opts.modelId}". If the user asks what model or provider you are using, answer with these exact runtime values. Do not infer your model identity from training data or from the name of another coding tool.` : ``;
1729
1239
  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.
1730
1240
 
1731
1241
  # Greetings
@@ -1736,18 +1246,16 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
1736
1246
  - read: read a file
1737
1247
  - write: create or overwrite a file
1738
1248
  - edit: targeted string replacement in an existing file (you must read the file first)
1739
- - brave_search: search the web
1249
+ - web_search: search the web
1740
1250
  - fetch_url: fetch and convert a URL to markdown
1741
1251
  - spawn_worker: dispatch a subagent for an isolated task
1742
- - spawn_coder: spawn a long-lived coding agent (Claude Code or Codex CLI) for code changes, file edits, debugging
1743
- - prompt_coder: send a follow-up prompt to a coder you previously spawned
1744
1252
  ${docsTools}${skillsTools}
1745
1253
 
1746
1254
  # Working with files
1747
1255
  - Prefer edit over write when modifying existing files.
1748
1256
  - You must read a file before you can edit it.
1749
1257
  - Use absolute paths or paths relative to the current working directory.
1750
- ${docsGuidance}${skillsGuidance}${onboardingGuidance}${docsUrlGuidance}
1258
+ ${modelGuidance}${docsGuidance}${skillsGuidance}${onboardingGuidance}${docsUrlGuidance}
1751
1259
 
1752
1260
  # Risky actions
1753
1261
  Pause and confirm with the user before:
@@ -1768,13 +1276,6 @@ When you spawn a worker, write its system prompt the way you'd brief a colleague
1768
1276
 
1769
1277
  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.
1770
1278
 
1771
- # When to spawn a coder
1772
- Spawn a coder when the user asks for code changes, file edits, debugging, or any task that benefits from a real coding agent with full tool access (bash, file edits, etc.). A coder runs Claude Code or Codex CLI under the hood.
1773
-
1774
- Unlike a worker, a coder is **long-lived**: its URL stays valid across many turns. Spawn once with spawn_coder, then keep prompting it via prompt_coder for follow-ups — don't spawn a new coder for each turn. Treat the coder URL like a chat handle.
1775
-
1776
- After calling spawn_coder or prompt_coder, end your turn. When the coder's reply lands, you'll be woken with the response in the wake message — relay it (or a summary) back to the user, and call prompt_coder again if there's a follow-up.
1777
-
1778
1279
  # Reporting
1779
1280
  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.
1780
1281
 
@@ -1788,34 +1289,82 @@ function createHortonTools(workingDirectory, ctx, readSet, opts = {}) {
1788
1289
  (0, __electric_ax_agents_runtime_tools.createWriteTool)(workingDirectory, readSet),
1789
1290
  (0, __electric_ax_agents_runtime_tools.createEditTool)(workingDirectory, readSet),
1790
1291
  __electric_ax_agents_runtime_tools.braveSearchTool,
1791
- __electric_ax_agents_runtime_tools.fetchUrlTool,
1792
- createSpawnWorkerTool(ctx),
1793
- createSpawnCoderTool(ctx),
1794
- createPromptCoderTool(ctx),
1292
+ ...opts.modelCatalog && opts.modelConfig ? [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)({
1293
+ catalog: opts.modelCatalog,
1294
+ modelConfig: opts.modelConfig,
1295
+ log: (message) => serverLog.info(message),
1296
+ logPrefix: opts.logPrefix ?? `[horton]`
1297
+ })] : [__electric_ax_agents_runtime_tools.fetchUrlTool],
1298
+ createSpawnWorkerTool(ctx, opts.modelConfig),
1795
1299
  ...opts.docsSearchTool ? [opts.docsSearchTool] : []
1796
1300
  ];
1797
1301
  }
1798
- function extractFirstUserMessage(events) {
1799
- for (const event of events) {
1800
- if (event.type !== `message_received`) continue;
1801
- const value = event.value;
1802
- if (!value || value.from === `system`) continue;
1803
- const payload = value.payload;
1804
- if (typeof payload === `string`) return payload;
1805
- if (payload != null) return JSON.stringify(payload);
1302
+ function payloadToTitleText(payload) {
1303
+ if (typeof payload === `string`) return payload;
1304
+ if (payload == null) return ``;
1305
+ if (typeof payload === `object`) {
1306
+ const text = payload.text;
1307
+ return typeof text === `string` ? text : JSON.stringify(payload);
1308
+ }
1309
+ return String(payload);
1310
+ }
1311
+ async function extractFirstUserMessage(ctx) {
1312
+ const firstMessage = await (0, __durable_streams_state.queryOnce)((q) => q.from({ inbox: ctx.db.collections.inbox }).where(({ inbox }) => (0, __durable_streams_state.not)((0, __durable_streams_state.eq)(inbox.from, `system`))).orderBy(({ inbox }) => inbox._seq, `asc`).findOne());
1313
+ if (!firstMessage) return null;
1314
+ const text = payloadToTitleText(firstMessage.payload);
1315
+ return text.length > 0 ? text : null;
1316
+ }
1317
+ function readAgentsMd(workingDirectory) {
1318
+ const agentsMdPath = node_path.default.join(workingDirectory, `AGENTS.md`);
1319
+ try {
1320
+ if (!node_fs.default.existsSync(agentsMdPath) || !node_fs.default.statSync(agentsMdPath).isFile()) return null;
1321
+ const content = node_fs.default.readFileSync(agentsMdPath, `utf8`);
1322
+ return [
1323
+ `<context_file kind="instructions" path="${agentsMdPath}">`,
1324
+ content,
1325
+ `</context_file>`
1326
+ ].join(`\n`);
1327
+ } catch {
1328
+ return null;
1806
1329
  }
1807
- return null;
1808
1330
  }
1809
1331
  function createAssistantHandler(options) {
1810
- const { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry, docsUrl } = options;
1332
+ const { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
1811
1333
  const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0);
1812
1334
  return async function assistantHandler(ctx, wake) {
1813
1335
  const readSet = new Set();
1336
+ const effectiveCwd = typeof ctx.args.workingDirectory === `string` && ctx.args.workingDirectory.trim().length > 0 ? ctx.args.workingDirectory : workingDirectory;
1337
+ const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
1338
+ const agentsMd = readAgentsMd(effectiveCwd);
1814
1339
  const tools = [
1815
1340
  ...ctx.electricTools,
1816
- ...createHortonTools(workingDirectory, ctx, readSet, { docsSearchTool }),
1817
- ...skillsRegistry && skillsRegistry.catalog.size > 0 ? createSkillTools(skillsRegistry, ctx) : []
1341
+ ...createHortonTools(effectiveCwd, ctx, readSet, {
1342
+ docsSearchTool,
1343
+ modelConfig,
1344
+ modelCatalog,
1345
+ logPrefix: `[horton ${ctx.entityUrl}]`
1346
+ }),
1347
+ ...skillsRegistry && skillsRegistry.catalog.size > 0 ? createSkillTools(skillsRegistry, ctx) : [],
1348
+ ...__electric_ax_agents_mcp.mcp.tools()
1818
1349
  ];
1350
+ const titlePromise = ctx.firstWake && !ctx.tags.title ? (async () => {
1351
+ const firstUserMessage = await extractFirstUserMessage(ctx);
1352
+ if (!firstUserMessage) return;
1353
+ let title = null;
1354
+ try {
1355
+ const result = await generateTitle(firstUserMessage, createConfiguredTitleCall(modelCatalog, modelConfig, `[horton ${ctx.entityUrl}]`), (reason) => {
1356
+ serverLog.warn(`[horton ${ctx.entityUrl}] title generation fell back to local title: ${reason}`);
1357
+ });
1358
+ if (result.length > 0) title = result;
1359
+ } catch (err) {
1360
+ serverLog.warn(`[horton ${ctx.entityUrl}] title generation failed: ${err instanceof Error ? err.message : String(err)}`);
1361
+ }
1362
+ if (title !== null) try {
1363
+ await ctx.setTag(`title`, title);
1364
+ } catch (err) {
1365
+ serverLog.warn(`[horton ${ctx.entityUrl}] setTag failed: ${err instanceof Error ? err.message : String(err)}`);
1366
+ }
1367
+ })() : Promise.resolve();
1819
1368
  if (docsSupport) ctx.useContext({
1820
1369
  sourceBudget: 1e5,
1821
1370
  sources: {
@@ -1833,6 +1382,11 @@ function createAssistantHandler(options) {
1833
1382
  content: () => ctx.timelineMessages(),
1834
1383
  cache: `volatile`
1835
1384
  },
1385
+ ...agentsMd ? { agents_md: {
1386
+ content: () => agentsMd,
1387
+ max: 2e4,
1388
+ cache: `stable`
1389
+ } } : {},
1836
1390
  ...skillsRegistry && skillsRegistry.catalog.size > 0 ? { skills_catalog: {
1837
1391
  content: () => skillsRegistry.renderCatalog(2e3),
1838
1392
  max: 2e3,
@@ -1851,41 +1405,46 @@ function createAssistantHandler(options) {
1851
1405
  conversation: {
1852
1406
  content: () => ctx.timelineMessages(),
1853
1407
  cache: `volatile`
1408
+ },
1409
+ ...agentsMd ? { agents_md: {
1410
+ content: () => agentsMd,
1411
+ max: 2e4,
1412
+ cache: `stable`
1413
+ } } : {}
1414
+ }
1415
+ });
1416
+ else if (agentsMd) ctx.useContext({
1417
+ sourceBudget: 1e5,
1418
+ sources: {
1419
+ conversation: {
1420
+ content: () => ctx.timelineMessages(),
1421
+ cache: `volatile`
1422
+ },
1423
+ agents_md: {
1424
+ content: () => agentsMd,
1425
+ max: 2e4,
1426
+ cache: `stable`
1854
1427
  }
1855
1428
  }
1856
1429
  });
1857
1430
  ctx.useAgent({
1858
- systemPrompt: buildHortonSystemPrompt(workingDirectory, {
1431
+ systemPrompt: buildHortonSystemPrompt(effectiveCwd, {
1859
1432
  hasDocsSupport: Boolean(docsSupport),
1860
1433
  hasSkills,
1861
- docsUrl
1434
+ docsUrl,
1435
+ modelProvider: modelConfig.provider,
1436
+ modelId: String(modelConfig.model)
1862
1437
  }),
1863
- model: HORTON_MODEL,
1438
+ ...modelConfig,
1864
1439
  tools,
1865
1440
  ...streamFn && { streamFn }
1866
1441
  });
1867
1442
  await ctx.agent.run();
1868
- if (ctx.firstWake && !ctx.tags.title) {
1869
- const firstUserMessage = extractFirstUserMessage(ctx.events);
1870
- if (firstUserMessage) {
1871
- let title = null;
1872
- try {
1873
- const result = await generateTitle(firstUserMessage);
1874
- if (result.length > 0) title = result;
1875
- } catch (err) {
1876
- serverLog.warn(`[horton ${ctx.entityUrl}] title generation failed: ${err instanceof Error ? err.message : String(err)}`);
1877
- }
1878
- if (title !== null) try {
1879
- await ctx.setTag(`title`, title);
1880
- } catch (err) {
1881
- serverLog.warn(`[horton ${ctx.entityUrl}] setTag failed: ${err instanceof Error ? err.message : String(err)}`);
1882
- }
1883
- }
1884
- }
1443
+ await titlePromise;
1885
1444
  };
1886
1445
  }
1887
1446
  function registerHorton(registry, options) {
1888
- const { workingDirectory, streamFn, skillsRegistry = null } = options;
1447
+ const { workingDirectory, streamFn, skillsRegistry = null, modelCatalog } = options;
1889
1448
  const docsUrl = options.docsUrl ?? process.env.HORTON_DOCS_URL;
1890
1449
  if (process.env.BRAVE_SEARCH_API_KEY) serverLog.info(`[horton] Web search: using Brave Search API`);
1891
1450
  else serverLog.warn(`[horton] BRAVE_SEARCH_API_KEY not set — web search will fall back to Anthropic built-in search (uses your ANTHROPIC_API_KEY)`);
@@ -1900,10 +1459,17 @@ function registerHorton(registry, options) {
1900
1459
  docsSupport,
1901
1460
  docsSearchTool,
1902
1461
  skillsRegistry,
1462
+ modelCatalog,
1903
1463
  docsUrl
1904
1464
  });
1465
+ const hortonCreationSchema = zod.z.object({
1466
+ model: zod.z.enum(modelChoiceValues(modelCatalog)).default(modelCatalog.defaultChoice.value),
1467
+ reasoningEffort: zod.z.enum(REASONING_EFFORT_VALUES).default(`auto`).describe(`Reasoning effort for compatible reasoning models. Auto uses a safe provider default.`),
1468
+ workingDirectory: zod.z.string().optional().describe(`Working directory for file operations. Defaults to the server's configured cwd.`)
1469
+ });
1905
1470
  registry.define(`horton`, {
1906
1471
  description: `Friendly capable assistant — chat, code, research, dispatch`,
1472
+ creationSchema: hortonCreationSchema,
1907
1473
  handler: assistantHandler
1908
1474
  });
1909
1475
  const typeNames = [`horton`];
@@ -1948,6 +1514,9 @@ function parseWorkerArgs(value) {
1948
1514
  };
1949
1515
  }
1950
1516
  if (tools.length === 0 && !args.sharedDb) throw new Error(`[worker] must provide tools and/or sharedDb`);
1517
+ if (typeof value.model === `string`) args.model = value.model;
1518
+ if (typeof value.provider === `string`) args.provider = value.provider;
1519
+ if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
1951
1520
  return args;
1952
1521
  }
1953
1522
  function buildToolsForWorker(tools, workingDirectory, ctx, readSet) {
@@ -1965,7 +1534,7 @@ function buildToolsForWorker(tools, workingDirectory, ctx, readSet) {
1965
1534
  case `edit`:
1966
1535
  out.push((0, __electric_ax_agents_runtime_tools.createEditTool)(workingDirectory, readSet));
1967
1536
  break;
1968
- case `brave_search`:
1537
+ case `web_search`:
1969
1538
  out.push(__electric_ax_agents_runtime_tools.braveSearchTool);
1970
1539
  break;
1971
1540
  case `fetch_url`:
@@ -2074,13 +1643,14 @@ function buildSharedStateTools(shared, schema, mode) {
2074
1643
  return tools;
2075
1644
  }
2076
1645
  function registerWorker(registry, options) {
2077
- const { workingDirectory, streamFn } = options;
1646
+ const { workingDirectory, streamFn, modelCatalog } = options;
2078
1647
  registry.define(`worker`, {
2079
1648
  description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
2080
1649
  async handler(ctx) {
2081
1650
  const args = parseWorkerArgs(ctx.args);
2082
1651
  const readSet = new Set();
2083
1652
  const builtinTools = buildToolsForWorker(args.tools, workingDirectory, ctx, readSet);
1653
+ const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
2084
1654
  const sharedStateTools = [];
2085
1655
  if (args.sharedDb) {
2086
1656
  const shared = await ctx.observe((0, __electric_ax_agents_runtime.db)(args.sharedDb.id, args.sharedDb.schema));
@@ -2088,7 +1658,7 @@ function registerWorker(registry, options) {
2088
1658
  }
2089
1659
  ctx.useAgent({
2090
1660
  systemPrompt: `${args.systemPrompt}${WORKER_PROMPT_FOOTER}`,
2091
- model: HORTON_MODEL,
1661
+ ...modelConfig,
2092
1662
  tools: [...builtinTools, ...sharedStateTools],
2093
1663
  ...streamFn && { streamFn }
2094
1664
  });
@@ -2178,7 +1748,6 @@ function stripQuotes(value) {
2178
1748
 
2179
1749
  //#endregion
2180
1750
  //#region src/skills/extract-meta.ts
2181
- const EXTRACT_MODEL = `claude-haiku-4-5-20251001`;
2182
1751
  const DEFAULT_MAX = 1e4;
2183
1752
  async function extractSkillMeta(name, content) {
2184
1753
  const preamble = parsePreamble(content);
@@ -2191,7 +1760,7 @@ async function extractSkillMeta(name, content) {
2191
1760
  ...preamble.userInvocable && { userInvocable: true },
2192
1761
  max: preamble.max ?? DEFAULT_MAX
2193
1762
  };
2194
- if (process.env.ANTHROPIC_API_KEY) try {
1763
+ try {
2195
1764
  return await llmExtract(name, content, preamble);
2196
1765
  } catch (err) {
2197
1766
  serverLog.warn(`[skills] LLM metadata extraction failed for "${name}": ${err instanceof Error ? err.message : String(err)}`);
@@ -2204,7 +1773,6 @@ async function extractSkillMeta(name, content) {
2204
1773
  };
2205
1774
  }
2206
1775
  async function llmExtract(name, content, partial) {
2207
- const client = new __anthropic_ai_sdk.default();
2208
1776
  const truncated = content.slice(0, 8e3);
2209
1777
  const prompt = `Analyze this skill document and extract metadata. The skill is named "${name}".
2210
1778
 
@@ -2218,15 +1786,14 @@ Return ONLY a JSON object with these fields:
2218
1786
  - "keywords": array of 3-8 relevant keywords
2219
1787
 
2220
1788
  Return raw JSON, no markdown fences.`;
2221
- const res = await client.messages.create({
2222
- model: EXTRACT_MODEL,
2223
- max_tokens: 256,
2224
- messages: [{
2225
- role: `user`,
2226
- content: prompt
2227
- }]
1789
+ const text = await (0, __electric_ax_agents_runtime.completeWithLowCostModel)({
1790
+ purpose: `skill metadata extraction`,
1791
+ systemPrompt: `Extract metadata from skill documents. Return only valid JSON that matches the requested schema.`,
1792
+ prompt,
1793
+ maxTokens: 256,
1794
+ log: (message) => serverLog.info(message),
1795
+ logPrefix: `[skills]`
2228
1796
  });
2229
- const text = res.content[0]?.type === `text` ? res.content[0].text : ``;
2230
1797
  const parsed = JSON.parse(text);
2231
1798
  return {
2232
1799
  description: partial.description ?? parsed.description ?? humanize(name),
@@ -2357,9 +1924,10 @@ function truncate(str, max) {
2357
1924
  //#region src/bootstrap.ts
2358
1925
  const DEFAULT_BUILTIN_AGENT_HANDLER_PATH = `/_electric/builtin-agent-handler`;
2359
1926
  async function createBuiltinAgentHandler(options) {
2360
- const { agentServerUrl, serveEndpoint = `${agentServerUrl}${DEFAULT_BUILTIN_AGENT_HANDLER_PATH}`, workingDirectory, streamFn, createElectricTools } = options;
2361
- if (!streamFn && !process.env.ANTHROPIC_API_KEY) {
2362
- serverLog.warn(`[builtin-agents] ANTHROPIC_API_KEY not set — skipping built-in agent registration`);
1927
+ const { agentServerUrl, serveEndpoint = `${agentServerUrl}${DEFAULT_BUILTIN_AGENT_HANDLER_PATH}`, workingDirectory, streamFn, createElectricTools, publicUrl, runtimeName } = options;
1928
+ const modelCatalog = await createBuiltinModelCatalog({ allowMockFallback: Boolean(streamFn) });
1929
+ if (!modelCatalog) {
1930
+ serverLog.warn(`[builtin-agents] no supported model provider API key found — set ANTHROPIC_API_KEY or OPENAI_API_KEY`);
2363
1931
  return null;
2364
1932
  }
2365
1933
  const cwd = workingDirectory ?? process.cwd();
@@ -2380,22 +1948,24 @@ async function createBuiltinAgentHandler(options) {
2380
1948
  const typeNames = registerHorton(registry, {
2381
1949
  workingDirectory: cwd,
2382
1950
  streamFn,
2383
- skillsRegistry
1951
+ skillsRegistry,
1952
+ modelCatalog
2384
1953
  });
2385
1954
  registerWorker(registry, {
2386
1955
  workingDirectory: cwd,
2387
- streamFn
1956
+ streamFn,
1957
+ modelCatalog
2388
1958
  });
2389
1959
  typeNames.push(`worker`);
2390
- registerCodingSession(registry, { defaultWorkingDirectory: cwd });
2391
- typeNames.push(`coder`);
2392
1960
  const runtime = (0, __electric_ax_agents_runtime.createRuntimeHandler)({
2393
1961
  baseUrl: agentServerUrl,
2394
1962
  serveEndpoint,
2395
1963
  registry,
2396
1964
  subscriptionPathForType: (name) => `/${name}/*/main`,
2397
1965
  idleTimeout: 5e3,
2398
- createElectricTools
1966
+ createElectricTools,
1967
+ publicUrl,
1968
+ name: runtimeName ?? `builtin-agents`
2399
1969
  });
2400
1970
  return {
2401
1971
  handler: runtime.onEnter,
@@ -2427,10 +1997,19 @@ var BuiltinAgentsServer = class {
2427
1997
  bootstrap = null;
2428
1998
  _url = null;
2429
1999
  publicBaseUrl = null;
2000
+ _mcpRegistry = null;
2001
+ mcpWatcherCloser = null;
2002
+ mcpToolProviderName = null;
2003
+ mcpApplyInFlight = new Set();
2004
+ mcpStopping = false;
2430
2005
  options;
2431
2006
  constructor(options) {
2432
2007
  this.options = options;
2433
2008
  }
2009
+ /** Embedded MCP registry. `null` until `start()` has run. */
2010
+ get mcpRegistry() {
2011
+ return this._mcpRegistry;
2012
+ }
2434
2013
  get url() {
2435
2014
  if (!this._url) throw new Error(`Builtin agents server not started`);
2436
2015
  return this._url;
@@ -2464,14 +2043,124 @@ var BuiltinAgentsServer = class {
2464
2043
  this.publicBaseUrl = this.options.baseUrl ?? this._url;
2465
2044
  const webhookPath = this.options.webhookPath ?? DEFAULT_BUILTIN_AGENT_HANDLER_PATH;
2466
2045
  const serveEndpoint = new URL(webhookPath, this.publicBaseUrl.endsWith(`/`) ? this.publicBaseUrl : `${this.publicBaseUrl}/`).toString();
2046
+ const publicUrl = this.options.mcpOAuthRedirectBase ?? this.publicBaseUrl;
2047
+ const mcpRegistry = (0, __electric_ax_agents_mcp.createRegistry)({
2048
+ publicUrl,
2049
+ openAuthorizeUrl: this.options.openAuthorizeUrl
2050
+ });
2051
+ this._mcpRegistry = mcpRegistry;
2052
+ const mcpConfigPath = this.options.loadProjectMcpConfig ? node_path.default.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
2053
+ const extras = this.options.extraMcpServers ?? [];
2054
+ const wirePersistence = async (cfg) => {
2055
+ const servers = [];
2056
+ for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
2057
+ const persist = await (0, __electric_ax_agents_mcp.keychainPersistence)({ server: s.name });
2058
+ servers.push({
2059
+ ...s,
2060
+ auth: {
2061
+ ...s.auth,
2062
+ ...persist
2063
+ }
2064
+ });
2065
+ } else servers.push(s);
2066
+ return {
2067
+ ...cfg,
2068
+ servers
2069
+ };
2070
+ };
2071
+ const merge = (jsonCfg) => {
2072
+ const jsonServers = jsonCfg?.servers ?? [];
2073
+ const jsonNames = new Set(jsonServers.map((s) => s.name));
2074
+ const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
2075
+ return {
2076
+ servers: [...filteredExtras, ...jsonServers],
2077
+ raw: jsonCfg?.raw
2078
+ };
2079
+ };
2080
+ const onConfigError = this.options.onConfigError;
2081
+ const runApply = async (jsonCfg) => {
2082
+ if (this.mcpStopping) return;
2083
+ try {
2084
+ const wired = await wirePersistence(merge(jsonCfg));
2085
+ if (this.mcpStopping) return;
2086
+ await mcpRegistry.applyConfig(wired);
2087
+ } catch (e) {
2088
+ serverLog.error(`[mcp] applyConfig:`, e);
2089
+ try {
2090
+ onConfigError?.(e);
2091
+ } catch (cbErr) {
2092
+ serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
2093
+ }
2094
+ }
2095
+ };
2096
+ const applyMerged = (jsonCfg) => {
2097
+ const p = runApply(jsonCfg);
2098
+ this.mcpApplyInFlight.add(p);
2099
+ p.finally(() => this.mcpApplyInFlight.delete(p));
2100
+ return p;
2101
+ };
2102
+ if (mcpConfigPath) {
2103
+ try {
2104
+ const cfg = await (0, __electric_ax_agents_mcp.loadConfig)(mcpConfigPath, process.env);
2105
+ applyMerged(cfg);
2106
+ } catch (err) {
2107
+ if (err.code !== `ENOENT`) throw err;
2108
+ if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
2109
+ else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
2110
+ applyMerged(null);
2111
+ }
2112
+ try {
2113
+ this.mcpWatcherCloser = await (0, __electric_ax_agents_mcp.watchConfig)(mcpConfigPath, {
2114
+ onChange: (cfg) => void applyMerged(cfg),
2115
+ onError: (e) => serverLog.error(`[mcp] config error:`, e)
2116
+ });
2117
+ } catch (e) {
2118
+ serverLog.error(`[mcp] config watcher failed to start:`, e);
2119
+ }
2120
+ } else {
2121
+ if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
2122
+ applyMerged(null);
2123
+ }
2124
+ this.mcpToolProviderName = `mcp`;
2125
+ (0, __electric_ax_agents_runtime.registerToolProvider)({
2126
+ name: `mcp`,
2127
+ tools: () => {
2128
+ const tools = [];
2129
+ for (const entry of mcpRegistry.list()) {
2130
+ if (entry.status !== `ready`) continue;
2131
+ const live = mcpRegistry.get(entry.name);
2132
+ if (!live?.transport) continue;
2133
+ for (const t of entry.tools) tools.push((0, __electric_ax_agents_mcp.bridgeMcpTool)({
2134
+ server: entry.name,
2135
+ tool: t,
2136
+ client: live.transport.client,
2137
+ timeoutMs: live.config.timeoutMs
2138
+ }));
2139
+ const caps = live.transport.client.getServerCapabilities?.();
2140
+ if (caps?.resources) tools.push(...(0, __electric_ax_agents_mcp.buildResourceTools)({
2141
+ server: entry.name,
2142
+ client: live.transport.client,
2143
+ timeoutMs: live.config.timeoutMs
2144
+ }));
2145
+ if (caps?.prompts) tools.push(...(0, __electric_ax_agents_mcp.buildPromptTools)({
2146
+ server: entry.name,
2147
+ client: live.transport.client,
2148
+ timeoutMs: live.config.timeoutMs
2149
+ }));
2150
+ }
2151
+ return tools;
2152
+ }
2153
+ });
2467
2154
  this.bootstrap = await createBuiltinAgentHandler({
2468
2155
  agentServerUrl: this.options.agentServerUrl,
2469
2156
  serveEndpoint,
2470
2157
  workingDirectory: this.options.workingDirectory,
2471
2158
  streamFn: this.options.mockStreamFn,
2472
- createElectricTools: this.options.createElectricTools
2159
+ createElectricTools: this.options.createElectricTools,
2160
+ publicUrl,
2161
+ runtimeName: `builtin-agents`
2473
2162
  });
2474
- if (!this.bootstrap) throw new Error(`ANTHROPIC_API_KEY must be set before starting builtin agents`);
2163
+ if (!this.bootstrap) throw new Error(`ANTHROPIC_API_KEY or OPENAI_API_KEY must be set before starting builtin agents`);
2475
2164
  await registerBuiltinAgentTypes(this.bootstrap);
2476
2165
  serverLog.info(`[builtin-agents] webhook handler listening at ${serveEndpoint}`);
2477
2166
  resolve(this._url);
@@ -2488,6 +2177,26 @@ var BuiltinAgentsServer = class {
2488
2177
  await Promise.race([this.bootstrap.runtime.drainWakes().catch(() => {}), new Promise((resolve) => setTimeout(resolve, 5e3))]);
2489
2178
  this.bootstrap = null;
2490
2179
  }
2180
+ this.mcpStopping = true;
2181
+ if (this.mcpWatcherCloser) {
2182
+ try {
2183
+ this.mcpWatcherCloser();
2184
+ } catch (e) {
2185
+ serverLog.error(`[mcp] watcher close failed:`, e);
2186
+ }
2187
+ this.mcpWatcherCloser = null;
2188
+ }
2189
+ if (this.mcpApplyInFlight.size > 0) await Promise.allSettled([...this.mcpApplyInFlight]);
2190
+ if (this.mcpToolProviderName) {
2191
+ (0, __electric_ax_agents_runtime.unregisterToolProvider)(this.mcpToolProviderName);
2192
+ this.mcpToolProviderName = null;
2193
+ }
2194
+ if (this._mcpRegistry) {
2195
+ await this._mcpRegistry.close().catch((e) => {
2196
+ serverLog.error(`[mcp] registry close failed:`, e);
2197
+ });
2198
+ this._mcpRegistry = null;
2199
+ }
2491
2200
  if (this.server) {
2492
2201
  const server = this.server;
2493
2202
  await new Promise((resolve) => {
@@ -2495,19 +2204,20 @@ var BuiltinAgentsServer = class {
2495
2204
  });
2496
2205
  this.server = null;
2497
2206
  }
2207
+ this.mcpStopping = false;
2498
2208
  this._url = null;
2499
2209
  this.publicBaseUrl = null;
2500
2210
  }
2501
2211
  async handleRequest(req, res) {
2502
2212
  const method = req.method?.toUpperCase();
2503
- const path$6 = new URL(req.url ?? `/`, `http://localhost`).pathname;
2213
+ const pathname = new URL(req.url ?? `/`, `http://localhost`).pathname;
2504
2214
  const webhookPath = this.options.webhookPath ?? DEFAULT_BUILTIN_AGENT_HANDLER_PATH;
2505
- if (path$6 === `/_electric/health` && method === `GET`) {
2215
+ if (pathname === `/_electric/health` && method === `GET`) {
2506
2216
  res.writeHead(200, { "content-type": `application/json` });
2507
2217
  res.end(JSON.stringify({ status: `ok` }));
2508
2218
  return;
2509
2219
  }
2510
- if (path$6 === webhookPath && method === `POST` && this.bootstrap) {
2220
+ if (pathname === webhookPath && method === `POST` && this.bootstrap) {
2511
2221
  await this.bootstrap.handler(req, res);
2512
2222
  return;
2513
2223
  }
@@ -2589,7 +2299,6 @@ exports.createSpawnWorkerTool = createSpawnWorkerTool
2589
2299
  exports.generateTitle = generateTitle
2590
2300
  exports.registerAgentTypes = registerAgentTypes
2591
2301
  exports.registerBuiltinAgentTypes = registerBuiltinAgentTypes
2592
- exports.registerCodingSession = registerCodingSession
2593
2302
  exports.registerHorton = registerHorton
2594
2303
  exports.registerWorker = registerWorker
2595
2304
  exports.resolveBuiltinAgentsEntrypointOptions = resolveBuiltinAgentsEntrypointOptions