@electric-ax/agents 0.2.3 → 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/entrypoint.js +474 -737
- package/dist/index.cjs +470 -733
- package/dist/index.d.cts +68 -35
- package/dist/index.d.ts +69 -36
- package/dist/index.js +489 -751
- package/docs/entities/agents/horton.md +12 -12
- package/docs/entities/agents/worker.md +18 -18
- package/docs/entities/patterns/blackboard.md +6 -6
- package/docs/entities/patterns/dispatcher.md +1 -1
- package/docs/entities/patterns/manager-worker.md +1 -1
- package/docs/entities/patterns/map-reduce.md +1 -1
- package/docs/entities/patterns/pipeline.md +1 -1
- package/docs/entities/patterns/reactive-observers.md +2 -2
- package/docs/examples/playground.md +42 -26
- package/docs/index.md +25 -23
- package/docs/quickstart.md +12 -12
- package/docs/reference/agent-config.md +20 -12
- package/docs/reference/agent-tool.md +1 -1
- package/docs/reference/built-in-collections.md +21 -21
- package/docs/reference/cli.md +39 -30
- package/docs/reference/entity-definition.md +9 -9
- package/docs/reference/entity-handle.md +2 -2
- package/docs/reference/entity-registry.md +1 -1
- package/docs/reference/handler-context.md +34 -18
- package/docs/reference/mcp-registry.md +189 -0
- package/docs/reference/mcp-server-config.md +226 -0
- package/docs/reference/runtime-handler.md +25 -23
- package/docs/reference/shared-state-handle.md +7 -7
- package/docs/reference/state-collection-proxy.md +1 -1
- package/docs/reference/wake-event.md +23 -23
- package/docs/usage/app-setup.md +24 -23
- package/docs/usage/clients-and-react.md +40 -36
- package/docs/usage/configuring-the-agent.md +25 -19
- package/docs/usage/context-composition.md +12 -12
- package/docs/usage/defining-entities.md +36 -36
- package/docs/usage/defining-tools.md +45 -45
- package/docs/usage/embedded-builtins.md +54 -43
- package/docs/usage/managing-state.md +12 -12
- package/docs/usage/mcp-servers.md +354 -0
- package/docs/usage/overview.md +50 -45
- package/docs/usage/programmatic-runtime-client.md +51 -48
- package/docs/usage/shared-state.md +32 -32
- package/docs/usage/spawning-and-coordinating.md +9 -9
- package/docs/usage/testing.md +14 -14
- package/docs/usage/waking-entities.md +13 -13
- package/docs/usage/writing-handlers.md +52 -26
- package/package.json +9 -4
- package/scripts/sync-docs.mjs +42 -0
- package/docs/examples/mega-draw.md +0 -106
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
|
|
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
|
|
@@ -71,6 +70,10 @@ function formatArgs(args) {
|
|
|
71
70
|
};
|
|
72
71
|
}
|
|
73
72
|
const serverLog = {
|
|
73
|
+
debug(...args) {
|
|
74
|
+
const { msg } = formatArgs(args);
|
|
75
|
+
logger.debug(msg);
|
|
76
|
+
},
|
|
74
77
|
info(...args) {
|
|
75
78
|
const { msg } = formatArgs(args);
|
|
76
79
|
logger.info(msg);
|
|
@@ -90,516 +93,6 @@ const serverLog = {
|
|
|
90
93
|
}
|
|
91
94
|
};
|
|
92
95
|
|
|
93
|
-
//#endregion
|
|
94
|
-
//#region src/agents/coding-session.ts
|
|
95
|
-
const defaultCliRunner = { async run(opts) {
|
|
96
|
-
return new Promise((resolve, reject) => {
|
|
97
|
-
const isClaude = opts.agent === `claude`;
|
|
98
|
-
const bin = isClaude ? `claude` : `codex`;
|
|
99
|
-
const args = isClaude ? opts.sessionId ? [
|
|
100
|
-
`-r`,
|
|
101
|
-
opts.sessionId,
|
|
102
|
-
`--dangerously-skip-permissions`,
|
|
103
|
-
`-p`
|
|
104
|
-
] : [`--dangerously-skip-permissions`, `-p`] : opts.sessionId ? [
|
|
105
|
-
`exec`,
|
|
106
|
-
`--skip-git-repo-check`,
|
|
107
|
-
`resume`,
|
|
108
|
-
opts.sessionId,
|
|
109
|
-
opts.prompt
|
|
110
|
-
] : [
|
|
111
|
-
`exec`,
|
|
112
|
-
`--skip-git-repo-check`,
|
|
113
|
-
opts.prompt
|
|
114
|
-
];
|
|
115
|
-
const child = (0, node_child_process.spawn)(bin, args, {
|
|
116
|
-
cwd: opts.cwd,
|
|
117
|
-
stdio: [
|
|
118
|
-
isClaude ? `pipe` : `ignore`,
|
|
119
|
-
`pipe`,
|
|
120
|
-
`pipe`
|
|
121
|
-
]
|
|
122
|
-
});
|
|
123
|
-
const MAX_BUF_CHARS = 4096;
|
|
124
|
-
let stdout = ``;
|
|
125
|
-
let stderr = ``;
|
|
126
|
-
child.stdout?.on(`data`, (d) => {
|
|
127
|
-
if (stdout.length < MAX_BUF_CHARS) stdout += d.toString().slice(0, MAX_BUF_CHARS - stdout.length);
|
|
128
|
-
});
|
|
129
|
-
child.stderr?.on(`data`, (d) => {
|
|
130
|
-
if (stderr.length < MAX_BUF_CHARS) stderr += d.toString().slice(0, MAX_BUF_CHARS - stderr.length);
|
|
131
|
-
});
|
|
132
|
-
child.on(`error`, reject);
|
|
133
|
-
child.on(`exit`, (code) => {
|
|
134
|
-
resolve({
|
|
135
|
-
exitCode: code ?? -1,
|
|
136
|
-
stdout,
|
|
137
|
-
stderr
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
if (isClaude && child.stdin) {
|
|
141
|
-
child.stdin.write(opts.prompt);
|
|
142
|
-
child.stdin.end();
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
} };
|
|
146
|
-
async function discoverNewestSession(agent, cwd, excludeIds) {
|
|
147
|
-
const all = await (0, agent_session_protocol.discoverSessions)(agent);
|
|
148
|
-
const candidates = all.filter((s) => !excludeIds.has(s.sessionId) && (!s.cwd || s.cwd === cwd));
|
|
149
|
-
if (candidates.length === 0) return null;
|
|
150
|
-
return candidates[0].sessionId;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Compute the candidate directories where Claude Code stores per-cwd
|
|
154
|
-
* session JSONL files. Claude resolves the cwd to its realpath when
|
|
155
|
-
* choosing the directory name (so /tmp/foo on macOS lands under
|
|
156
|
-
* `-private-tmp-foo`), but the entity may have been spawned with the
|
|
157
|
-
* non-realpath form. Return both candidates so the caller can union
|
|
158
|
-
* their contents.
|
|
159
|
-
*/
|
|
160
|
-
async function getClaudeProjectDirs(cwd) {
|
|
161
|
-
const home = (0, node_os.homedir)();
|
|
162
|
-
const make = (c) => node_path.default.join(home, `.claude`, `projects`, c.replace(/\//g, `-`));
|
|
163
|
-
const dirs = [make(cwd)];
|
|
164
|
-
try {
|
|
165
|
-
const real = await node_fs.promises.realpath(cwd);
|
|
166
|
-
if (real !== cwd) dirs.push(make(real));
|
|
167
|
-
} catch {}
|
|
168
|
-
return dirs;
|
|
169
|
-
}
|
|
170
|
-
async function listClaudeJsonlIdsByCwd(cwd) {
|
|
171
|
-
const ids = new Set();
|
|
172
|
-
for (const dir of await getClaudeProjectDirs(cwd)) try {
|
|
173
|
-
const files = await node_fs.promises.readdir(dir);
|
|
174
|
-
for (const f of files) if (f.endsWith(`.jsonl`)) ids.add(f.slice(0, -`.jsonl`.length));
|
|
175
|
-
} catch {}
|
|
176
|
-
return ids;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Deterministic-path discovery for a freshly created session. After the
|
|
180
|
-
* Claude CLI runs in `-p` mode it writes the new JSONL straight into
|
|
181
|
-
* `~/.claude/projects/<sanitize(cwd)>/<id>.jsonl` *without* leaving a
|
|
182
|
-
* `~/.claude/sessions/<pid>.json` lock file (those are interactive-only),
|
|
183
|
-
* so `discoverSessions` can miss it. Compute the expected dir directly
|
|
184
|
-
* and diff its contents against a pre-run snapshot. Returns the newest
|
|
185
|
-
* fresh sessionId or null. Codex falls back to discoverNewestSession.
|
|
186
|
-
*/
|
|
187
|
-
async function findNewSessionAfterRun(agent, cwd, preDirectIds, preDiscoveredIds) {
|
|
188
|
-
if (agent === `claude`) {
|
|
189
|
-
const dirs = await getClaudeProjectDirs(cwd);
|
|
190
|
-
let best = null;
|
|
191
|
-
for (const dir of dirs) try {
|
|
192
|
-
const files = await node_fs.promises.readdir(dir);
|
|
193
|
-
for (const f of files) {
|
|
194
|
-
if (!f.endsWith(`.jsonl`)) continue;
|
|
195
|
-
const id = f.slice(0, -`.jsonl`.length);
|
|
196
|
-
if (preDirectIds.has(id)) continue;
|
|
197
|
-
const st = await node_fs.promises.stat(node_path.default.join(dir, f)).catch(() => null);
|
|
198
|
-
if (!st) continue;
|
|
199
|
-
if (!best || st.mtimeMs > best.mtime) best = {
|
|
200
|
-
id,
|
|
201
|
-
mtime: st.mtimeMs
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
} catch {}
|
|
205
|
-
if (best) return best.id;
|
|
206
|
-
}
|
|
207
|
-
return discoverNewestSession(agent, cwd, preDiscoveredIds);
|
|
208
|
-
}
|
|
209
|
-
const sessionMetaRowSchema = zod.z.object({
|
|
210
|
-
key: zod.z.literal(`current`),
|
|
211
|
-
electricSessionId: zod.z.string(),
|
|
212
|
-
nativeSessionId: zod.z.string().optional(),
|
|
213
|
-
agent: zod.z.enum([`claude`, `codex`]),
|
|
214
|
-
cwd: zod.z.string(),
|
|
215
|
-
status: zod.z.enum([
|
|
216
|
-
`initializing`,
|
|
217
|
-
`idle`,
|
|
218
|
-
`running`,
|
|
219
|
-
`error`
|
|
220
|
-
]),
|
|
221
|
-
error: zod.z.string().optional(),
|
|
222
|
-
currentPromptInboxKey: zod.z.string().optional()
|
|
223
|
-
});
|
|
224
|
-
const cursorStateRowSchema = zod.z.object({
|
|
225
|
-
key: zod.z.literal(`current`),
|
|
226
|
-
cursor: zod.z.string(),
|
|
227
|
-
lastProcessedInboxKey: zod.z.string().optional()
|
|
228
|
-
});
|
|
229
|
-
const eventRowSchema = zod.z.object({
|
|
230
|
-
key: zod.z.string(),
|
|
231
|
-
ts: zod.z.number(),
|
|
232
|
-
type: zod.z.string(),
|
|
233
|
-
callId: zod.z.string().optional(),
|
|
234
|
-
payload: zod.z.looseObject({})
|
|
235
|
-
});
|
|
236
|
-
const creationArgsSchema = zod.z.object({
|
|
237
|
-
agent: zod.z.enum([`claude`, `codex`]),
|
|
238
|
-
cwd: zod.z.string().optional(),
|
|
239
|
-
nativeSessionId: zod.z.string().optional(),
|
|
240
|
-
importFrom: zod.z.object({
|
|
241
|
-
agent: zod.z.enum([`claude`, `codex`]),
|
|
242
|
-
sessionId: zod.z.string()
|
|
243
|
-
}).optional()
|
|
244
|
-
});
|
|
245
|
-
const promptMessageSchema = zod.z.object({ text: zod.z.string() });
|
|
246
|
-
/**
|
|
247
|
-
* Stable key for an events-collection row, derived from the event's content.
|
|
248
|
-
* Lets us re-insert the same event without producing duplicates — the caller
|
|
249
|
-
* (or the collection's uniqueness guard) uses this to de-dup across retries,
|
|
250
|
-
* replays, and crash recovery. Sorts chronologically by ts, then by type.
|
|
251
|
-
*/
|
|
252
|
-
function eventKey(event) {
|
|
253
|
-
const tsPart = String(event.ts).padStart(16, `0`);
|
|
254
|
-
return `${tsPart}_${event.type}_${contentHashHex(event)}`;
|
|
255
|
-
}
|
|
256
|
-
function contentHashHex(event) {
|
|
257
|
-
const json = JSON.stringify(event);
|
|
258
|
-
let h = 5381;
|
|
259
|
-
for (let i = 0; i < json.length; i++) h = (h * 33 ^ json.charCodeAt(i)) >>> 0;
|
|
260
|
-
return h.toString(16).padStart(8, `0`);
|
|
261
|
-
}
|
|
262
|
-
function buildEventRow(event) {
|
|
263
|
-
const callId = `callId` in event && typeof event.callId === `string` ? event.callId : void 0;
|
|
264
|
-
return {
|
|
265
|
-
key: eventKey(event),
|
|
266
|
-
ts: event.ts,
|
|
267
|
-
type: event.type,
|
|
268
|
-
...callId !== void 0 ? { callId } : {},
|
|
269
|
-
payload: event
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
function appendIfNew(ctx, event) {
|
|
273
|
-
const row = buildEventRow(event);
|
|
274
|
-
if (ctx.events.get(row.key) !== void 0) return;
|
|
275
|
-
ctx.actions.events_insert({ row });
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Mirror every event that lands in the JSONL file while `runWork` is
|
|
279
|
-
* executing (i.e. while the CLI is running). Returns the advanced cursor
|
|
280
|
-
* and the `runWork` result once everything has settled and every append
|
|
281
|
-
* has been persisted to the entity's durable stream.
|
|
282
|
-
*
|
|
283
|
-
* If setup fails (e.g. the session file can't be resolved), `runWork`
|
|
284
|
-
* still runs — but nothing is mirrored and `setupError` is populated so
|
|
285
|
-
* the caller can surface the condition. If `runWork` throws, the error
|
|
286
|
-
* propagates after the watcher has been cleaned up.
|
|
287
|
-
*/
|
|
288
|
-
async function runWithLiveMirror(opts) {
|
|
289
|
-
let cursor = null;
|
|
290
|
-
let setupError = void 0;
|
|
291
|
-
try {
|
|
292
|
-
const session = await (0, agent_session_protocol.resolveSession)(opts.nativeSessionId, opts.agent);
|
|
293
|
-
if (opts.serializedCursor) cursor = (0, agent_session_protocol.deserializeCursor)({
|
|
294
|
-
...opts.serializedCursor,
|
|
295
|
-
path: session.path
|
|
296
|
-
});
|
|
297
|
-
else {
|
|
298
|
-
const initial = await (0, agent_session_protocol.loadSession)({
|
|
299
|
-
sessionId: opts.nativeSessionId,
|
|
300
|
-
agent: opts.agent
|
|
301
|
-
});
|
|
302
|
-
for (const ev of initial.events) appendIfNew(opts.ctx, ev);
|
|
303
|
-
cursor = initial.cursor;
|
|
304
|
-
}
|
|
305
|
-
} catch (e) {
|
|
306
|
-
setupError = e;
|
|
307
|
-
}
|
|
308
|
-
if (!cursor) {
|
|
309
|
-
const result$1 = await opts.runWork();
|
|
310
|
-
return {
|
|
311
|
-
cursor: opts.serializedCursor,
|
|
312
|
-
setupError,
|
|
313
|
-
result: result$1
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
let activeCursor = cursor;
|
|
317
|
-
let busy = false;
|
|
318
|
-
let pending = false;
|
|
319
|
-
let stopped = false;
|
|
320
|
-
const drainOnce = async () => {
|
|
321
|
-
if (stopped && busy) return;
|
|
322
|
-
if (busy) {
|
|
323
|
-
pending = true;
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
busy = true;
|
|
327
|
-
try {
|
|
328
|
-
const res = await (0, agent_session_protocol.tailSession)({ cursor: activeCursor });
|
|
329
|
-
activeCursor = res.cursor;
|
|
330
|
-
for (const ev of res.newEvents) appendIfNew(opts.ctx, ev);
|
|
331
|
-
} catch {} finally {
|
|
332
|
-
busy = false;
|
|
333
|
-
if (pending && !stopped) {
|
|
334
|
-
pending = false;
|
|
335
|
-
drainOnce();
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
const fileWatcher = (0, node_fs.watch)(activeCursor.path, () => {
|
|
340
|
-
drainOnce();
|
|
341
|
-
});
|
|
342
|
-
const pollHandle = setInterval(() => {
|
|
343
|
-
drainOnce();
|
|
344
|
-
}, 1500);
|
|
345
|
-
let result;
|
|
346
|
-
try {
|
|
347
|
-
result = await opts.runWork();
|
|
348
|
-
} finally {
|
|
349
|
-
stopped = true;
|
|
350
|
-
clearInterval(pollHandle);
|
|
351
|
-
fileWatcher.close();
|
|
352
|
-
while (busy) await new Promise((r) => setTimeout(r, 10));
|
|
353
|
-
try {
|
|
354
|
-
const final = await (0, agent_session_protocol.tailSession)({ cursor: activeCursor });
|
|
355
|
-
activeCursor = final.cursor;
|
|
356
|
-
for (const ev of final.newEvents) appendIfNew(opts.ctx, ev);
|
|
357
|
-
} catch {}
|
|
358
|
-
}
|
|
359
|
-
return {
|
|
360
|
-
cursor: (0, agent_session_protocol.serializeCursor)(activeCursor),
|
|
361
|
-
setupError,
|
|
362
|
-
result
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
function registerCodingSession(registry, options = {}) {
|
|
366
|
-
const runner = options.cliRunner ?? defaultCliRunner;
|
|
367
|
-
const defaultCwd = options.defaultWorkingDirectory ?? process.cwd();
|
|
368
|
-
registry.define(`coder`, {
|
|
369
|
-
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.`,
|
|
370
|
-
creationSchema: creationArgsSchema,
|
|
371
|
-
inboxSchemas: { prompt: promptMessageSchema },
|
|
372
|
-
state: {
|
|
373
|
-
sessionMeta: {
|
|
374
|
-
schema: sessionMetaRowSchema,
|
|
375
|
-
type: __electric_ax_agents_runtime.CODING_SESSION_META_COLLECTION_TYPE,
|
|
376
|
-
primaryKey: `key`
|
|
377
|
-
},
|
|
378
|
-
cursorState: {
|
|
379
|
-
schema: cursorStateRowSchema,
|
|
380
|
-
type: __electric_ax_agents_runtime.CODING_SESSION_CURSOR_COLLECTION_TYPE,
|
|
381
|
-
primaryKey: `key`
|
|
382
|
-
},
|
|
383
|
-
events: {
|
|
384
|
-
schema: eventRowSchema,
|
|
385
|
-
type: __electric_ax_agents_runtime.CODING_SESSION_EVENT_COLLECTION_TYPE,
|
|
386
|
-
primaryKey: `key`
|
|
387
|
-
}
|
|
388
|
-
},
|
|
389
|
-
async handler(ctx, _wake) {
|
|
390
|
-
const existingMeta = ctx.db.collections.sessionMeta.get(`current`);
|
|
391
|
-
if (!existingMeta) {
|
|
392
|
-
const args = creationArgsSchema.parse(ctx.args);
|
|
393
|
-
const cwd = args.cwd ?? defaultCwd;
|
|
394
|
-
const electricSessionId = ctx.entityUrl.split(`/`).pop() ?? ctx.entityUrl;
|
|
395
|
-
let resolvedNativeId = args.nativeSessionId;
|
|
396
|
-
if (args.importFrom) {
|
|
397
|
-
const result = await (0, agent_session_protocol.importLocalSession)({
|
|
398
|
-
source: {
|
|
399
|
-
sessionId: args.importFrom.sessionId,
|
|
400
|
-
agent: args.importFrom.agent
|
|
401
|
-
},
|
|
402
|
-
target: {
|
|
403
|
-
agent: args.agent,
|
|
404
|
-
cwd
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
resolvedNativeId = result.sessionId;
|
|
408
|
-
}
|
|
409
|
-
const hasNative = resolvedNativeId !== void 0;
|
|
410
|
-
ctx.db.actions.sessionMeta_insert({ row: {
|
|
411
|
-
key: `current`,
|
|
412
|
-
electricSessionId,
|
|
413
|
-
...hasNative ? { nativeSessionId: resolvedNativeId } : {},
|
|
414
|
-
agent: args.agent,
|
|
415
|
-
cwd,
|
|
416
|
-
status: hasNative ? `idle` : `initializing`
|
|
417
|
-
} });
|
|
418
|
-
}
|
|
419
|
-
if (!ctx.db.collections.cursorState.get(`current`)) ctx.db.actions.cursorState_insert({ row: {
|
|
420
|
-
key: `current`,
|
|
421
|
-
cursor: ``
|
|
422
|
-
} });
|
|
423
|
-
const metaRow = ctx.db.collections.sessionMeta.get(`current`);
|
|
424
|
-
const cursorRow = ctx.db.collections.cursorState.get(`current`);
|
|
425
|
-
if (!metaRow || !cursorRow) throw new Error(`[coding-session] expected sessionMeta and cursorState rows to exist after init`);
|
|
426
|
-
if (metaRow.nativeSessionId && !cursorRow.cursor) {
|
|
427
|
-
const mirrorCtx = {
|
|
428
|
-
events: { get: (k) => ctx.db.collections.events.get(k) },
|
|
429
|
-
actions: { events_insert: ctx.db.actions.events_insert }
|
|
430
|
-
};
|
|
431
|
-
try {
|
|
432
|
-
const initial = await (0, agent_session_protocol.loadSession)({
|
|
433
|
-
sessionId: metaRow.nativeSessionId,
|
|
434
|
-
agent: metaRow.agent
|
|
435
|
-
});
|
|
436
|
-
for (const ev of initial.events) appendIfNew(mirrorCtx, ev);
|
|
437
|
-
const serialized = (0, agent_session_protocol.serializeCursor)(initial.cursor);
|
|
438
|
-
ctx.db.actions.cursorState_update({
|
|
439
|
-
key: `current`,
|
|
440
|
-
updater: (d) => {
|
|
441
|
-
d.cursor = JSON.stringify(serialized);
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
} catch (e) {
|
|
445
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
446
|
-
ctx.db.actions.sessionMeta_update({
|
|
447
|
-
key: `current`,
|
|
448
|
-
updater: (d) => {
|
|
449
|
-
d.error = `initial mirror failed: ${message}`;
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
const inboxRows = ctx.db.collections.inbox.toArray.slice().sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
455
|
-
const lastKey = cursorRow.lastProcessedInboxKey ?? ``;
|
|
456
|
-
const pending = inboxRows.filter((m) => m.key > lastKey);
|
|
457
|
-
if (pending.length === 0) {
|
|
458
|
-
if (metaRow.status === `running` || metaRow.status === `error`) ctx.db.actions.sessionMeta_update({
|
|
459
|
-
key: `current`,
|
|
460
|
-
updater: (d) => {
|
|
461
|
-
d.status = `idle`;
|
|
462
|
-
delete d.currentPromptInboxKey;
|
|
463
|
-
delete d.error;
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
let runningMeta = metaRow;
|
|
469
|
-
let runningCursor = cursorRow;
|
|
470
|
-
for (const inboxMsg of pending) {
|
|
471
|
-
const parsed = promptMessageSchema.safeParse(inboxMsg.payload);
|
|
472
|
-
if (!parsed.success) {
|
|
473
|
-
ctx.db.actions.cursorState_update({
|
|
474
|
-
key: `current`,
|
|
475
|
-
updater: (d) => {
|
|
476
|
-
d.lastProcessedInboxKey = inboxMsg.key;
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
runningCursor = {
|
|
480
|
-
...runningCursor,
|
|
481
|
-
lastProcessedInboxKey: inboxMsg.key
|
|
482
|
-
};
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
const prompt = parsed.data.text;
|
|
486
|
-
const existingTitle = ctx.tags.title;
|
|
487
|
-
if (typeof existingTitle !== `string` || existingTitle.length === 0) ctx.setTag(`title`, prompt.slice(0, 80));
|
|
488
|
-
ctx.db.actions.sessionMeta_update({
|
|
489
|
-
key: `current`,
|
|
490
|
-
updater: (d) => {
|
|
491
|
-
d.status = `running`;
|
|
492
|
-
d.currentPromptInboxKey = inboxMsg.key;
|
|
493
|
-
delete d.error;
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
const recordedRun = ctx.recordRun();
|
|
497
|
-
const eventKeysBefore = new Set(ctx.db.collections.events.toArray.map((e) => e.key));
|
|
498
|
-
try {
|
|
499
|
-
const mirrorCtx = {
|
|
500
|
-
events: { get: (k) => ctx.db.collections.events.get(k) },
|
|
501
|
-
actions: { events_insert: ctx.db.actions.events_insert }
|
|
502
|
-
};
|
|
503
|
-
let nextCursorJson = runningCursor.cursor;
|
|
504
|
-
if (!runningMeta.nativeSessionId) {
|
|
505
|
-
const preDirectIds = runningMeta.agent === `claude` ? await listClaudeJsonlIdsByCwd(runningMeta.cwd) : new Set();
|
|
506
|
-
const preDiscoveredIds = new Set((await (0, agent_session_protocol.discoverSessions)(runningMeta.agent)).map((s) => s.sessionId));
|
|
507
|
-
const cliResult = await runner.run({
|
|
508
|
-
agent: runningMeta.agent,
|
|
509
|
-
cwd: runningMeta.cwd,
|
|
510
|
-
prompt
|
|
511
|
-
});
|
|
512
|
-
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>`}`);
|
|
513
|
-
const foundId = await findNewSessionAfterRun(runningMeta.agent, runningMeta.cwd, preDirectIds, preDiscoveredIds);
|
|
514
|
-
if (!foundId) throw new Error(`[coding-session] ${runningMeta.agent} CLI succeeded but no new session file was found`);
|
|
515
|
-
ctx.db.actions.sessionMeta_update({
|
|
516
|
-
key: `current`,
|
|
517
|
-
updater: (d) => {
|
|
518
|
-
d.nativeSessionId = foundId;
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
runningMeta = {
|
|
522
|
-
...runningMeta,
|
|
523
|
-
nativeSessionId: foundId
|
|
524
|
-
};
|
|
525
|
-
const initial = await (0, agent_session_protocol.loadSession)({
|
|
526
|
-
sessionId: foundId,
|
|
527
|
-
agent: runningMeta.agent
|
|
528
|
-
});
|
|
529
|
-
for (const ev of initial.events) appendIfNew(mirrorCtx, ev);
|
|
530
|
-
nextCursorJson = JSON.stringify((0, agent_session_protocol.serializeCursor)(initial.cursor));
|
|
531
|
-
} else {
|
|
532
|
-
const serializedCursor = runningCursor.cursor ? JSON.parse(runningCursor.cursor) : null;
|
|
533
|
-
const { cursor: nextSerialized, setupError, result: cliResult } = await runWithLiveMirror({
|
|
534
|
-
agent: runningMeta.agent,
|
|
535
|
-
nativeSessionId: runningMeta.nativeSessionId,
|
|
536
|
-
serializedCursor,
|
|
537
|
-
ctx: mirrorCtx,
|
|
538
|
-
runWork: () => runner.run({
|
|
539
|
-
agent: runningMeta.agent,
|
|
540
|
-
sessionId: runningMeta.nativeSessionId,
|
|
541
|
-
cwd: runningMeta.cwd,
|
|
542
|
-
prompt
|
|
543
|
-
})
|
|
544
|
-
});
|
|
545
|
-
if (setupError) throw setupError instanceof Error ? setupError : new Error(String(setupError));
|
|
546
|
-
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>`}`);
|
|
547
|
-
const persistedCursor = nextSerialized ?? serializedCursor;
|
|
548
|
-
nextCursorJson = persistedCursor ? JSON.stringify(persistedCursor) : ``;
|
|
549
|
-
}
|
|
550
|
-
ctx.db.actions.cursorState_update({
|
|
551
|
-
key: `current`,
|
|
552
|
-
updater: (d) => {
|
|
553
|
-
d.cursor = nextCursorJson;
|
|
554
|
-
d.lastProcessedInboxKey = inboxMsg.key;
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
|
-
runningCursor = {
|
|
558
|
-
...runningCursor,
|
|
559
|
-
cursor: nextCursorJson,
|
|
560
|
-
lastProcessedInboxKey: inboxMsg.key
|
|
561
|
-
};
|
|
562
|
-
for (const row of ctx.db.collections.events.toArray) {
|
|
563
|
-
if (eventKeysBefore.has(row.key)) continue;
|
|
564
|
-
if (row.type !== `assistant_message`) continue;
|
|
565
|
-
const text = row.payload?.text;
|
|
566
|
-
if (typeof text === `string` && text.length > 0) recordedRun.attachResponse(text);
|
|
567
|
-
}
|
|
568
|
-
recordedRun.end({ status: `completed` });
|
|
569
|
-
} catch (e) {
|
|
570
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
571
|
-
recordedRun.end({
|
|
572
|
-
status: `failed`,
|
|
573
|
-
finishReason: `error`
|
|
574
|
-
});
|
|
575
|
-
ctx.db.actions.sessionMeta_update({
|
|
576
|
-
key: `current`,
|
|
577
|
-
updater: (d) => {
|
|
578
|
-
d.status = `error`;
|
|
579
|
-
d.error = message;
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
ctx.db.actions.cursorState_update({
|
|
583
|
-
key: `current`,
|
|
584
|
-
updater: (d) => {
|
|
585
|
-
d.lastProcessedInboxKey = inboxMsg.key;
|
|
586
|
-
}
|
|
587
|
-
});
|
|
588
|
-
throw e;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
ctx.db.actions.sessionMeta_update({
|
|
592
|
-
key: `current`,
|
|
593
|
-
updater: (d) => {
|
|
594
|
-
d.status = `idle`;
|
|
595
|
-
delete d.currentPromptInboxKey;
|
|
596
|
-
delete d.error;
|
|
597
|
-
}
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
|
|
603
96
|
//#endregion
|
|
604
97
|
//#region src/docs/embed.ts
|
|
605
98
|
const EMBEDDING_DIMENSIONS = 128;
|
|
@@ -815,15 +308,38 @@ function findLatestQuestion(items) {
|
|
|
815
308
|
return void 0;
|
|
816
309
|
}
|
|
817
310
|
function resolveDocsRoot(workingDirectory) {
|
|
311
|
+
const envDocsRoot = process.env.HORTON_DOCS_ROOT;
|
|
818
312
|
const candidates = [
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
313
|
+
envDocsRoot ? {
|
|
314
|
+
path: envDocsRoot,
|
|
315
|
+
requireIndex: false
|
|
316
|
+
} : null,
|
|
317
|
+
{
|
|
318
|
+
path: node_path.default.resolve(workingDirectory, `electric-agents-docs/docs`),
|
|
319
|
+
requireIndex: false
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
path: node_path.default.resolve(process.cwd(), `electric-agents-docs/docs`),
|
|
323
|
+
requireIndex: false
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
path: node_path.default.resolve(MODULE_DIR, `../docs`),
|
|
327
|
+
requireIndex: true
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
path: node_path.default.resolve(MODULE_DIR, `../../docs`),
|
|
331
|
+
requireIndex: true
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
path: node_path.default.resolve(MODULE_DIR, `../../../../website/docs/agents`),
|
|
335
|
+
requireIndex: true
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
path: node_path.default.resolve(MODULE_DIR, `../../../../../electric-agents-docs/docs`),
|
|
339
|
+
requireIndex: false
|
|
340
|
+
}
|
|
341
|
+
].filter((value) => Boolean(value));
|
|
342
|
+
for (const candidate of candidates) if (node_fs.default.existsSync(candidate.path) && (!candidate.requireIndex || node_fs.default.existsSync(node_path.default.join(candidate.path, `index.md`)))) return candidate.path;
|
|
827
343
|
return null;
|
|
828
344
|
}
|
|
829
345
|
var DocsKnowledgeBase = class {
|
|
@@ -853,7 +369,7 @@ var DocsKnowledgeBase = class {
|
|
|
853
369
|
return db$1;
|
|
854
370
|
} catch (error) {
|
|
855
371
|
const message = error instanceof Error ? error.message : String(error);
|
|
856
|
-
|
|
372
|
+
serverLog.debug(`${this.logPrefix} falling back to in-memory docs index: ${message}`);
|
|
857
373
|
return null;
|
|
858
374
|
}
|
|
859
375
|
}
|
|
@@ -940,7 +456,7 @@ var DocsKnowledgeBase = class {
|
|
|
940
456
|
}
|
|
941
457
|
this.fallbackFingerprint = fingerprint;
|
|
942
458
|
const stats$1 = this.stats();
|
|
943
|
-
|
|
459
|
+
serverLog.debug(`${this.logPrefix} indexed ${stats$1.docCount} docs into ${stats$1.chunkCount} chunks (${stats$1.fingerprint.slice(0, 12)}...)`);
|
|
944
460
|
return stats$1;
|
|
945
461
|
}
|
|
946
462
|
const db$1 = this.db;
|
|
@@ -977,7 +493,7 @@ var DocsKnowledgeBase = class {
|
|
|
977
493
|
});
|
|
978
494
|
reset();
|
|
979
495
|
const stats = this.stats();
|
|
980
|
-
|
|
496
|
+
serverLog.debug(`${this.logPrefix} indexed ${stats.docCount} docs into ${stats.chunkCount} chunks (${stats.fingerprint.slice(0, 12)}...)`);
|
|
981
497
|
return stats;
|
|
982
498
|
}
|
|
983
499
|
hybridSearch(query, limit = DEFAULT_K) {
|
|
@@ -1117,7 +633,7 @@ function renderSearchResults(query, results, docsRoot) {
|
|
|
1117
633
|
return lines.join(`\n`);
|
|
1118
634
|
}
|
|
1119
635
|
function logSearchResults(kind, query, output) {
|
|
1120
|
-
|
|
636
|
+
serverLog.debug(`[horton-docs] ${kind} search for "${query}"\n${output}\n`);
|
|
1121
637
|
}
|
|
1122
638
|
function createHortonDocsSupport(workingDirectory, opts = {}) {
|
|
1123
639
|
const docsRoot = opts.docsRoot ?? resolveDocsRoot(workingDirectory);
|
|
@@ -1381,11 +897,11 @@ const WORKER_TOOL_NAMES = [
|
|
|
1381
897
|
`read`,
|
|
1382
898
|
`write`,
|
|
1383
899
|
`edit`,
|
|
1384
|
-
`
|
|
900
|
+
`web_search`,
|
|
1385
901
|
`fetch_url`,
|
|
1386
902
|
`spawn_worker`
|
|
1387
903
|
];
|
|
1388
|
-
function createSpawnWorkerTool(ctx) {
|
|
904
|
+
function createSpawnWorkerTool(ctx, modelConfig) {
|
|
1389
905
|
return {
|
|
1390
906
|
name: `spawn_worker`,
|
|
1391
907
|
label: `Spawn Worker`,
|
|
@@ -1412,10 +928,16 @@ function createSpawnWorkerTool(ctx) {
|
|
|
1412
928
|
details: { spawned: false }
|
|
1413
929
|
};
|
|
1414
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
|
+
} : {};
|
|
1415
936
|
try {
|
|
1416
937
|
const handle = await ctx.spawn(`worker`, id, {
|
|
1417
938
|
systemPrompt,
|
|
1418
|
-
tools
|
|
939
|
+
tools,
|
|
940
|
+
...workerModelArgs
|
|
1419
941
|
}, {
|
|
1420
942
|
initialMessage,
|
|
1421
943
|
wake: {
|
|
@@ -1449,140 +971,138 @@ function createSpawnWorkerTool(ctx) {
|
|
|
1449
971
|
}
|
|
1450
972
|
|
|
1451
973
|
//#endregion
|
|
1452
|
-
//#region src/
|
|
1453
|
-
const
|
|
1454
|
-
|
|
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
|
+
};
|
|
1455
1004
|
return {
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
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.`,
|
|
1459
|
-
parameters: __sinclair_typebox.Type.Object({
|
|
1460
|
-
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.` }),
|
|
1461
|
-
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.` })),
|
|
1462
|
-
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.` }))
|
|
1463
|
-
}),
|
|
1464
|
-
execute: async (_toolCallId, params) => {
|
|
1465
|
-
const { prompt, agent, cwd } = params;
|
|
1466
|
-
if (typeof prompt !== `string` || prompt.length === 0) return {
|
|
1467
|
-
content: [{
|
|
1468
|
-
type: `text`,
|
|
1469
|
-
text: `Error: prompt is required and must be a non-empty string.`
|
|
1470
|
-
}],
|
|
1471
|
-
details: { spawned: false }
|
|
1472
|
-
};
|
|
1473
|
-
const id = (0, nanoid.nanoid)(10);
|
|
1474
|
-
const spawnArgs = { agent: agent ?? `claude` };
|
|
1475
|
-
if (cwd) spawnArgs.cwd = cwd;
|
|
1476
|
-
try {
|
|
1477
|
-
const handle = await ctx.spawn(`coder`, id, spawnArgs, {
|
|
1478
|
-
initialMessage: { text: prompt },
|
|
1479
|
-
wake: {
|
|
1480
|
-
on: `runFinished`,
|
|
1481
|
-
includeResponse: true
|
|
1482
|
-
}
|
|
1483
|
-
});
|
|
1484
|
-
const coderUrl = handle.entityUrl;
|
|
1485
|
-
return {
|
|
1486
|
-
content: [{
|
|
1487
|
-
type: `text`,
|
|
1488
|
-
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.`
|
|
1489
|
-
}],
|
|
1490
|
-
details: {
|
|
1491
|
-
spawned: true,
|
|
1492
|
-
coderUrl
|
|
1493
|
-
}
|
|
1494
|
-
};
|
|
1495
|
-
} catch (err) {
|
|
1496
|
-
serverLog.warn(`[spawn_coder tool] failed to spawn coder ${id}: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
|
|
1497
|
-
return {
|
|
1498
|
-
content: [{
|
|
1499
|
-
type: `text`,
|
|
1500
|
-
text: `Error spawning coder: ${err instanceof Error ? err.message : `Unknown error`}`
|
|
1501
|
-
}],
|
|
1502
|
-
details: { spawned: false }
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1005
|
+
choices: [fallback],
|
|
1006
|
+
defaultChoice: fallback
|
|
1506
1007
|
};
|
|
1507
1008
|
}
|
|
1508
|
-
function
|
|
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;
|
|
1509
1053
|
return {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
type: `text`,
|
|
1522
|
-
text: `Error: coder_url must be a path like "/coder/<id>".`
|
|
1523
|
-
}],
|
|
1524
|
-
details: { sent: false }
|
|
1525
|
-
};
|
|
1526
|
-
if (typeof prompt !== `string` || prompt.length === 0) return {
|
|
1527
|
-
content: [{
|
|
1528
|
-
type: `text`,
|
|
1529
|
-
text: `Error: prompt is required and must be a non-empty string.`
|
|
1530
|
-
}],
|
|
1531
|
-
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
|
+
}
|
|
1532
1065
|
};
|
|
1533
|
-
try {
|
|
1534
|
-
ctx.send(coder_url, { text: prompt });
|
|
1535
|
-
return {
|
|
1536
|
-
content: [{
|
|
1537
|
-
type: `text`,
|
|
1538
|
-
text: `Prompt queued for ${coder_url}. End your turn — you'll be woken when the coder's reply lands.`
|
|
1539
|
-
}],
|
|
1540
|
-
details: {
|
|
1541
|
-
sent: true,
|
|
1542
|
-
coderUrl: coder_url
|
|
1543
|
-
}
|
|
1544
|
-
};
|
|
1545
|
-
} catch (err) {
|
|
1546
|
-
serverLog.warn(`[prompt_coder tool] failed to send to ${coder_url}: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
|
|
1547
|
-
return {
|
|
1548
|
-
content: [{
|
|
1549
|
-
type: `text`,
|
|
1550
|
-
text: `Error sending prompt to coder: ${err instanceof Error ? err.message : `Unknown error`}`
|
|
1551
|
-
}],
|
|
1552
|
-
details: { sent: false }
|
|
1553
|
-
};
|
|
1554
|
-
}
|
|
1555
1066
|
}
|
|
1556
1067
|
};
|
|
1557
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
|
+
}
|
|
1558
1100
|
|
|
1559
1101
|
//#endregion
|
|
1560
1102
|
//#region src/agents/horton.ts
|
|
1561
|
-
const TITLE_MODEL = `claude-haiku-4-5-20251001`;
|
|
1562
1103
|
const HORTON_MODEL = `claude-sonnet-4-6`;
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
if (!anthropic) anthropic = new __anthropic_ai_sdk.default();
|
|
1566
|
-
return anthropic;
|
|
1567
|
-
}
|
|
1568
|
-
async function defaultHaikuCall(prompt) {
|
|
1569
|
-
const client = getClient();
|
|
1570
|
-
const res = await client.messages.create({
|
|
1571
|
-
model: TITLE_MODEL,
|
|
1572
|
-
max_tokens: 64,
|
|
1573
|
-
messages: [{
|
|
1574
|
-
role: `user`,
|
|
1575
|
-
content: prompt
|
|
1576
|
-
}]
|
|
1577
|
-
});
|
|
1578
|
-
const block = res.content[0];
|
|
1579
|
-
return block?.type === `text` ? block.text : ``;
|
|
1580
|
-
}
|
|
1581
|
-
const TITLE_PROMPT = (userMessage) => `Summarize the following user request in 3-5 words for use as a chat session title.
|
|
1582
|
-
Respond with only the title, no quotes, no punctuation, no preamble.
|
|
1583
|
-
|
|
1584
|
-
User request:
|
|
1585
|
-
${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}`;
|
|
1586
1106
|
const TITLE_STOP_WORDS = new Set([
|
|
1587
1107
|
`a`,
|
|
1588
1108
|
`an`,
|
|
@@ -1650,19 +1170,34 @@ function buildFallbackTitle(userMessage) {
|
|
|
1650
1170
|
const selected = informativeWords.length >= 2 ? informativeWords.slice(0, 5) : backupWords;
|
|
1651
1171
|
return selected.join(` `).slice(0, 80).trim() || `Untitled Chat`;
|
|
1652
1172
|
}
|
|
1653
|
-
|
|
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) {
|
|
1654
1186
|
try {
|
|
1655
|
-
const raw = await llmCall(
|
|
1187
|
+
const raw = await llmCall(TITLE_USER_PROMPT(userMessage));
|
|
1656
1188
|
const title = raw.trim();
|
|
1657
|
-
|
|
1658
|
-
|
|
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));
|
|
1659
1194
|
return buildFallbackTitle(userMessage);
|
|
1660
1195
|
}
|
|
1661
1196
|
}
|
|
1662
1197
|
function buildHortonSystemPrompt(workingDirectory, opts = {}) {
|
|
1663
1198
|
const docsTools = opts.hasDocsSupport ? `\n- search_durable_agents_docs: hybrid search over the built-in Durable Agents docs index` : ``;
|
|
1664
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` : ``;
|
|
1665
|
-
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
|
|
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.` : ``;
|
|
1666
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.
|
|
1667
1202
|
|
|
1668
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.
|
|
@@ -1698,7 +1233,9 @@ Don't force onboarding. If someone just wants to chat or code, let them. When in
|
|
|
1698
1233
|
- ${opts.hasDocsSupport ? `If search_durable_agents_docs is available, use it first (faster, hybrid search).` : `Use fetch_url to look up documentation pages.`}
|
|
1699
1234
|
- The Electric Agents docs site is at ${opts.docsUrl}
|
|
1700
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).
|
|
1701
|
-
- For general coding questions unrelated to Electric Agents, use
|
|
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.` : ``;
|
|
1702
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.
|
|
1703
1240
|
|
|
1704
1241
|
# Greetings
|
|
@@ -1709,18 +1246,16 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
|
|
|
1709
1246
|
- read: read a file
|
|
1710
1247
|
- write: create or overwrite a file
|
|
1711
1248
|
- edit: targeted string replacement in an existing file (you must read the file first)
|
|
1712
|
-
-
|
|
1249
|
+
- web_search: search the web
|
|
1713
1250
|
- fetch_url: fetch and convert a URL to markdown
|
|
1714
1251
|
- spawn_worker: dispatch a subagent for an isolated task
|
|
1715
|
-
- spawn_coder: spawn a long-lived coding agent (Claude Code or Codex CLI) for code changes, file edits, debugging
|
|
1716
|
-
- prompt_coder: send a follow-up prompt to a coder you previously spawned
|
|
1717
1252
|
${docsTools}${skillsTools}
|
|
1718
1253
|
|
|
1719
1254
|
# Working with files
|
|
1720
1255
|
- Prefer edit over write when modifying existing files.
|
|
1721
1256
|
- You must read a file before you can edit it.
|
|
1722
1257
|
- Use absolute paths or paths relative to the current working directory.
|
|
1723
|
-
${docsGuidance}${skillsGuidance}${onboardingGuidance}${docsUrlGuidance}
|
|
1258
|
+
${modelGuidance}${docsGuidance}${skillsGuidance}${onboardingGuidance}${docsUrlGuidance}
|
|
1724
1259
|
|
|
1725
1260
|
# Risky actions
|
|
1726
1261
|
Pause and confirm with the user before:
|
|
@@ -1741,13 +1276,6 @@ When you spawn a worker, write its system prompt the way you'd brief a colleague
|
|
|
1741
1276
|
|
|
1742
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.
|
|
1743
1278
|
|
|
1744
|
-
# When to spawn a coder
|
|
1745
|
-
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.
|
|
1746
|
-
|
|
1747
|
-
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.
|
|
1748
|
-
|
|
1749
|
-
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.
|
|
1750
|
-
|
|
1751
1279
|
# Reporting
|
|
1752
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.
|
|
1753
1281
|
|
|
@@ -1761,34 +1289,82 @@ function createHortonTools(workingDirectory, ctx, readSet, opts = {}) {
|
|
|
1761
1289
|
(0, __electric_ax_agents_runtime_tools.createWriteTool)(workingDirectory, readSet),
|
|
1762
1290
|
(0, __electric_ax_agents_runtime_tools.createEditTool)(workingDirectory, readSet),
|
|
1763
1291
|
__electric_ax_agents_runtime_tools.braveSearchTool,
|
|
1764
|
-
__electric_ax_agents_runtime_tools.
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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),
|
|
1768
1299
|
...opts.docsSearchTool ? [opts.docsSearchTool] : []
|
|
1769
1300
|
];
|
|
1770
1301
|
}
|
|
1771
|
-
function
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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;
|
|
1779
1329
|
}
|
|
1780
|
-
return null;
|
|
1781
1330
|
}
|
|
1782
1331
|
function createAssistantHandler(options) {
|
|
1783
|
-
const { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry, docsUrl } = options;
|
|
1332
|
+
const { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
|
|
1784
1333
|
const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0);
|
|
1785
1334
|
return async function assistantHandler(ctx, wake) {
|
|
1786
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);
|
|
1787
1339
|
const tools = [
|
|
1788
1340
|
...ctx.electricTools,
|
|
1789
|
-
...createHortonTools(
|
|
1790
|
-
|
|
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()
|
|
1791
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();
|
|
1792
1368
|
if (docsSupport) ctx.useContext({
|
|
1793
1369
|
sourceBudget: 1e5,
|
|
1794
1370
|
sources: {
|
|
@@ -1806,6 +1382,11 @@ function createAssistantHandler(options) {
|
|
|
1806
1382
|
content: () => ctx.timelineMessages(),
|
|
1807
1383
|
cache: `volatile`
|
|
1808
1384
|
},
|
|
1385
|
+
...agentsMd ? { agents_md: {
|
|
1386
|
+
content: () => agentsMd,
|
|
1387
|
+
max: 2e4,
|
|
1388
|
+
cache: `stable`
|
|
1389
|
+
} } : {},
|
|
1809
1390
|
...skillsRegistry && skillsRegistry.catalog.size > 0 ? { skills_catalog: {
|
|
1810
1391
|
content: () => skillsRegistry.renderCatalog(2e3),
|
|
1811
1392
|
max: 2e3,
|
|
@@ -1824,41 +1405,46 @@ function createAssistantHandler(options) {
|
|
|
1824
1405
|
conversation: {
|
|
1825
1406
|
content: () => ctx.timelineMessages(),
|
|
1826
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`
|
|
1827
1427
|
}
|
|
1828
1428
|
}
|
|
1829
1429
|
});
|
|
1830
1430
|
ctx.useAgent({
|
|
1831
|
-
systemPrompt: buildHortonSystemPrompt(
|
|
1431
|
+
systemPrompt: buildHortonSystemPrompt(effectiveCwd, {
|
|
1832
1432
|
hasDocsSupport: Boolean(docsSupport),
|
|
1833
1433
|
hasSkills,
|
|
1834
|
-
docsUrl
|
|
1434
|
+
docsUrl,
|
|
1435
|
+
modelProvider: modelConfig.provider,
|
|
1436
|
+
modelId: String(modelConfig.model)
|
|
1835
1437
|
}),
|
|
1836
|
-
|
|
1438
|
+
...modelConfig,
|
|
1837
1439
|
tools,
|
|
1838
1440
|
...streamFn && { streamFn }
|
|
1839
1441
|
});
|
|
1840
1442
|
await ctx.agent.run();
|
|
1841
|
-
|
|
1842
|
-
const firstUserMessage = extractFirstUserMessage(ctx.events);
|
|
1843
|
-
if (firstUserMessage) {
|
|
1844
|
-
let title = null;
|
|
1845
|
-
try {
|
|
1846
|
-
const result = await generateTitle(firstUserMessage);
|
|
1847
|
-
if (result.length > 0) title = result;
|
|
1848
|
-
} catch (err) {
|
|
1849
|
-
serverLog.warn(`[horton ${ctx.entityUrl}] title generation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1850
|
-
}
|
|
1851
|
-
if (title !== null) try {
|
|
1852
|
-
await ctx.setTag(`title`, title);
|
|
1853
|
-
} catch (err) {
|
|
1854
|
-
serverLog.warn(`[horton ${ctx.entityUrl}] setTag failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1443
|
+
await titlePromise;
|
|
1858
1444
|
};
|
|
1859
1445
|
}
|
|
1860
1446
|
function registerHorton(registry, options) {
|
|
1861
|
-
const { workingDirectory, streamFn, skillsRegistry = null } = options;
|
|
1447
|
+
const { workingDirectory, streamFn, skillsRegistry = null, modelCatalog } = options;
|
|
1862
1448
|
const docsUrl = options.docsUrl ?? process.env.HORTON_DOCS_URL;
|
|
1863
1449
|
if (process.env.BRAVE_SEARCH_API_KEY) serverLog.info(`[horton] Web search: using Brave Search API`);
|
|
1864
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)`);
|
|
@@ -1873,10 +1459,17 @@ function registerHorton(registry, options) {
|
|
|
1873
1459
|
docsSupport,
|
|
1874
1460
|
docsSearchTool,
|
|
1875
1461
|
skillsRegistry,
|
|
1462
|
+
modelCatalog,
|
|
1876
1463
|
docsUrl
|
|
1877
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
|
+
});
|
|
1878
1470
|
registry.define(`horton`, {
|
|
1879
1471
|
description: `Friendly capable assistant — chat, code, research, dispatch`,
|
|
1472
|
+
creationSchema: hortonCreationSchema,
|
|
1880
1473
|
handler: assistantHandler
|
|
1881
1474
|
});
|
|
1882
1475
|
const typeNames = [`horton`];
|
|
@@ -1921,6 +1514,9 @@ function parseWorkerArgs(value) {
|
|
|
1921
1514
|
};
|
|
1922
1515
|
}
|
|
1923
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;
|
|
1924
1520
|
return args;
|
|
1925
1521
|
}
|
|
1926
1522
|
function buildToolsForWorker(tools, workingDirectory, ctx, readSet) {
|
|
@@ -1938,7 +1534,7 @@ function buildToolsForWorker(tools, workingDirectory, ctx, readSet) {
|
|
|
1938
1534
|
case `edit`:
|
|
1939
1535
|
out.push((0, __electric_ax_agents_runtime_tools.createEditTool)(workingDirectory, readSet));
|
|
1940
1536
|
break;
|
|
1941
|
-
case `
|
|
1537
|
+
case `web_search`:
|
|
1942
1538
|
out.push(__electric_ax_agents_runtime_tools.braveSearchTool);
|
|
1943
1539
|
break;
|
|
1944
1540
|
case `fetch_url`:
|
|
@@ -2047,13 +1643,14 @@ function buildSharedStateTools(shared, schema, mode) {
|
|
|
2047
1643
|
return tools;
|
|
2048
1644
|
}
|
|
2049
1645
|
function registerWorker(registry, options) {
|
|
2050
|
-
const { workingDirectory, streamFn } = options;
|
|
1646
|
+
const { workingDirectory, streamFn, modelCatalog } = options;
|
|
2051
1647
|
registry.define(`worker`, {
|
|
2052
1648
|
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
|
|
2053
1649
|
async handler(ctx) {
|
|
2054
1650
|
const args = parseWorkerArgs(ctx.args);
|
|
2055
1651
|
const readSet = new Set();
|
|
2056
1652
|
const builtinTools = buildToolsForWorker(args.tools, workingDirectory, ctx, readSet);
|
|
1653
|
+
const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
|
|
2057
1654
|
const sharedStateTools = [];
|
|
2058
1655
|
if (args.sharedDb) {
|
|
2059
1656
|
const shared = await ctx.observe((0, __electric_ax_agents_runtime.db)(args.sharedDb.id, args.sharedDb.schema));
|
|
@@ -2061,7 +1658,7 @@ function registerWorker(registry, options) {
|
|
|
2061
1658
|
}
|
|
2062
1659
|
ctx.useAgent({
|
|
2063
1660
|
systemPrompt: `${args.systemPrompt}${WORKER_PROMPT_FOOTER}`,
|
|
2064
|
-
|
|
1661
|
+
...modelConfig,
|
|
2065
1662
|
tools: [...builtinTools, ...sharedStateTools],
|
|
2066
1663
|
...streamFn && { streamFn }
|
|
2067
1664
|
});
|
|
@@ -2151,7 +1748,6 @@ function stripQuotes(value) {
|
|
|
2151
1748
|
|
|
2152
1749
|
//#endregion
|
|
2153
1750
|
//#region src/skills/extract-meta.ts
|
|
2154
|
-
const EXTRACT_MODEL = `claude-haiku-4-5-20251001`;
|
|
2155
1751
|
const DEFAULT_MAX = 1e4;
|
|
2156
1752
|
async function extractSkillMeta(name, content) {
|
|
2157
1753
|
const preamble = parsePreamble(content);
|
|
@@ -2164,7 +1760,7 @@ async function extractSkillMeta(name, content) {
|
|
|
2164
1760
|
...preamble.userInvocable && { userInvocable: true },
|
|
2165
1761
|
max: preamble.max ?? DEFAULT_MAX
|
|
2166
1762
|
};
|
|
2167
|
-
|
|
1763
|
+
try {
|
|
2168
1764
|
return await llmExtract(name, content, preamble);
|
|
2169
1765
|
} catch (err) {
|
|
2170
1766
|
serverLog.warn(`[skills] LLM metadata extraction failed for "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -2177,7 +1773,6 @@ async function extractSkillMeta(name, content) {
|
|
|
2177
1773
|
};
|
|
2178
1774
|
}
|
|
2179
1775
|
async function llmExtract(name, content, partial) {
|
|
2180
|
-
const client = new __anthropic_ai_sdk.default();
|
|
2181
1776
|
const truncated = content.slice(0, 8e3);
|
|
2182
1777
|
const prompt = `Analyze this skill document and extract metadata. The skill is named "${name}".
|
|
2183
1778
|
|
|
@@ -2191,15 +1786,14 @@ Return ONLY a JSON object with these fields:
|
|
|
2191
1786
|
- "keywords": array of 3-8 relevant keywords
|
|
2192
1787
|
|
|
2193
1788
|
Return raw JSON, no markdown fences.`;
|
|
2194
|
-
const
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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]`
|
|
2201
1796
|
});
|
|
2202
|
-
const text = res.content[0]?.type === `text` ? res.content[0].text : ``;
|
|
2203
1797
|
const parsed = JSON.parse(text);
|
|
2204
1798
|
return {
|
|
2205
1799
|
description: partial.description ?? parsed.description ?? humanize(name),
|
|
@@ -2290,6 +1884,7 @@ async function saveCache(cachePath, catalog, cacheDir) {
|
|
|
2290
1884
|
const obj = {};
|
|
2291
1885
|
for (const [name, meta] of catalog) obj[name] = meta;
|
|
2292
1886
|
node_fs.default.mkdirSync(cacheDir, { recursive: true });
|
|
1887
|
+
await node_fs_promises.default.writeFile(node_path.default.join(cacheDir, `.gitignore`), `*\n`, `utf-8`);
|
|
2293
1888
|
await node_fs_promises.default.writeFile(cachePath, JSON.stringify(obj, null, 2), `utf-8`);
|
|
2294
1889
|
}
|
|
2295
1890
|
function sha256(content) {
|
|
@@ -2329,9 +1924,10 @@ function truncate(str, max) {
|
|
|
2329
1924
|
//#region src/bootstrap.ts
|
|
2330
1925
|
const DEFAULT_BUILTIN_AGENT_HANDLER_PATH = `/_electric/builtin-agent-handler`;
|
|
2331
1926
|
async function createBuiltinAgentHandler(options) {
|
|
2332
|
-
const { agentServerUrl, serveEndpoint = `${agentServerUrl}${DEFAULT_BUILTIN_AGENT_HANDLER_PATH}`, workingDirectory, streamFn, createElectricTools } = options;
|
|
2333
|
-
|
|
2334
|
-
|
|
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`);
|
|
2335
1931
|
return null;
|
|
2336
1932
|
}
|
|
2337
1933
|
const cwd = workingDirectory ?? process.cwd();
|
|
@@ -2352,22 +1948,24 @@ async function createBuiltinAgentHandler(options) {
|
|
|
2352
1948
|
const typeNames = registerHorton(registry, {
|
|
2353
1949
|
workingDirectory: cwd,
|
|
2354
1950
|
streamFn,
|
|
2355
|
-
skillsRegistry
|
|
1951
|
+
skillsRegistry,
|
|
1952
|
+
modelCatalog
|
|
2356
1953
|
});
|
|
2357
1954
|
registerWorker(registry, {
|
|
2358
1955
|
workingDirectory: cwd,
|
|
2359
|
-
streamFn
|
|
1956
|
+
streamFn,
|
|
1957
|
+
modelCatalog
|
|
2360
1958
|
});
|
|
2361
1959
|
typeNames.push(`worker`);
|
|
2362
|
-
registerCodingSession(registry, { defaultWorkingDirectory: cwd });
|
|
2363
|
-
typeNames.push(`coder`);
|
|
2364
1960
|
const runtime = (0, __electric_ax_agents_runtime.createRuntimeHandler)({
|
|
2365
1961
|
baseUrl: agentServerUrl,
|
|
2366
1962
|
serveEndpoint,
|
|
2367
1963
|
registry,
|
|
2368
1964
|
subscriptionPathForType: (name) => `/${name}/*/main`,
|
|
2369
1965
|
idleTimeout: 5e3,
|
|
2370
|
-
createElectricTools
|
|
1966
|
+
createElectricTools,
|
|
1967
|
+
publicUrl,
|
|
1968
|
+
name: runtimeName ?? `builtin-agents`
|
|
2371
1969
|
});
|
|
2372
1970
|
return {
|
|
2373
1971
|
handler: runtime.onEnter,
|
|
@@ -2399,10 +1997,19 @@ var BuiltinAgentsServer = class {
|
|
|
2399
1997
|
bootstrap = null;
|
|
2400
1998
|
_url = null;
|
|
2401
1999
|
publicBaseUrl = null;
|
|
2000
|
+
_mcpRegistry = null;
|
|
2001
|
+
mcpWatcherCloser = null;
|
|
2002
|
+
mcpToolProviderName = null;
|
|
2003
|
+
mcpApplyInFlight = new Set();
|
|
2004
|
+
mcpStopping = false;
|
|
2402
2005
|
options;
|
|
2403
2006
|
constructor(options) {
|
|
2404
2007
|
this.options = options;
|
|
2405
2008
|
}
|
|
2009
|
+
/** Embedded MCP registry. `null` until `start()` has run. */
|
|
2010
|
+
get mcpRegistry() {
|
|
2011
|
+
return this._mcpRegistry;
|
|
2012
|
+
}
|
|
2406
2013
|
get url() {
|
|
2407
2014
|
if (!this._url) throw new Error(`Builtin agents server not started`);
|
|
2408
2015
|
return this._url;
|
|
@@ -2436,14 +2043,124 @@ var BuiltinAgentsServer = class {
|
|
|
2436
2043
|
this.publicBaseUrl = this.options.baseUrl ?? this._url;
|
|
2437
2044
|
const webhookPath = this.options.webhookPath ?? DEFAULT_BUILTIN_AGENT_HANDLER_PATH;
|
|
2438
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
|
+
});
|
|
2439
2154
|
this.bootstrap = await createBuiltinAgentHandler({
|
|
2440
2155
|
agentServerUrl: this.options.agentServerUrl,
|
|
2441
2156
|
serveEndpoint,
|
|
2442
2157
|
workingDirectory: this.options.workingDirectory,
|
|
2443
2158
|
streamFn: this.options.mockStreamFn,
|
|
2444
|
-
createElectricTools: this.options.createElectricTools
|
|
2159
|
+
createElectricTools: this.options.createElectricTools,
|
|
2160
|
+
publicUrl,
|
|
2161
|
+
runtimeName: `builtin-agents`
|
|
2445
2162
|
});
|
|
2446
|
-
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`);
|
|
2447
2164
|
await registerBuiltinAgentTypes(this.bootstrap);
|
|
2448
2165
|
serverLog.info(`[builtin-agents] webhook handler listening at ${serveEndpoint}`);
|
|
2449
2166
|
resolve(this._url);
|
|
@@ -2460,6 +2177,26 @@ var BuiltinAgentsServer = class {
|
|
|
2460
2177
|
await Promise.race([this.bootstrap.runtime.drainWakes().catch(() => {}), new Promise((resolve) => setTimeout(resolve, 5e3))]);
|
|
2461
2178
|
this.bootstrap = null;
|
|
2462
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
|
+
}
|
|
2463
2200
|
if (this.server) {
|
|
2464
2201
|
const server = this.server;
|
|
2465
2202
|
await new Promise((resolve) => {
|
|
@@ -2467,19 +2204,20 @@ var BuiltinAgentsServer = class {
|
|
|
2467
2204
|
});
|
|
2468
2205
|
this.server = null;
|
|
2469
2206
|
}
|
|
2207
|
+
this.mcpStopping = false;
|
|
2470
2208
|
this._url = null;
|
|
2471
2209
|
this.publicBaseUrl = null;
|
|
2472
2210
|
}
|
|
2473
2211
|
async handleRequest(req, res) {
|
|
2474
2212
|
const method = req.method?.toUpperCase();
|
|
2475
|
-
const
|
|
2213
|
+
const pathname = new URL(req.url ?? `/`, `http://localhost`).pathname;
|
|
2476
2214
|
const webhookPath = this.options.webhookPath ?? DEFAULT_BUILTIN_AGENT_HANDLER_PATH;
|
|
2477
|
-
if (
|
|
2215
|
+
if (pathname === `/_electric/health` && method === `GET`) {
|
|
2478
2216
|
res.writeHead(200, { "content-type": `application/json` });
|
|
2479
2217
|
res.end(JSON.stringify({ status: `ok` }));
|
|
2480
2218
|
return;
|
|
2481
2219
|
}
|
|
2482
|
-
if (
|
|
2220
|
+
if (pathname === webhookPath && method === `POST` && this.bootstrap) {
|
|
2483
2221
|
await this.bootstrap.handler(req, res);
|
|
2484
2222
|
return;
|
|
2485
2223
|
}
|
|
@@ -2561,7 +2299,6 @@ exports.createSpawnWorkerTool = createSpawnWorkerTool
|
|
|
2561
2299
|
exports.generateTitle = generateTitle
|
|
2562
2300
|
exports.registerAgentTypes = registerAgentTypes
|
|
2563
2301
|
exports.registerBuiltinAgentTypes = registerBuiltinAgentTypes
|
|
2564
|
-
exports.registerCodingSession = registerCodingSession
|
|
2565
2302
|
exports.registerHorton = registerHorton
|
|
2566
2303
|
exports.registerWorker = registerWorker
|
|
2567
2304
|
exports.resolveBuiltinAgentsEntrypointOptions = resolveBuiltinAgentsEntrypointOptions
|