@electric-ax/agents 0.4.15 → 0.4.17
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 +125 -5
- package/dist/index.cjs +124 -4
- package/dist/index.js +125 -5
- package/package.json +3 -3
package/dist/entrypoint.js
CHANGED
|
@@ -4,7 +4,7 @@ import { cacheStores, getGlobalDispatcher, interceptors, setGlobalDispatcher } f
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import pino from "pino";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, buildSkillSlashCommands, completeWithLowCostModel, createContextSkillLoader, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModel, getMoonshotModels, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
7
|
+
import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, buildSkillSlashCommands, completeWithLowCostModel, createContextSkillLoader, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModel, getMoonshotModels, pgSync, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
8
8
|
import { braveSearchTool, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createScheduleTools, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
|
|
9
9
|
import { chooseDefaultSandbox, isE2BAvailable, lazySandbox, remoteSandbox } from "@electric-ax/agents-runtime/sandbox";
|
|
10
10
|
import { z } from "zod";
|
|
@@ -19,13 +19,19 @@ import { bridgeMcpTool, buildPromptTools, buildResourceTools, createRegistry, ke
|
|
|
19
19
|
|
|
20
20
|
//#region src/durable-streams-cache.ts
|
|
21
21
|
const MEMORY_CACHE_SIZE_BYTES = 100 * 1024 * 1024;
|
|
22
|
+
let installed = false;
|
|
22
23
|
function installDurableStreamsFetchCache(options = {}) {
|
|
23
24
|
if (options === false) return;
|
|
25
|
+
if (installed) {
|
|
26
|
+
console.warn(`[agents] installDurableStreamsFetchCache called more than once; ignoring`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
24
29
|
const store = options.store === `sqlite` || options.sqliteLocation ? new cacheStores.SqliteCacheStore({
|
|
25
30
|
location: options.sqliteLocation,
|
|
26
31
|
maxCount: options.maxCount
|
|
27
32
|
}) : new cacheStores.MemoryCacheStore({ maxSize: MEMORY_CACHE_SIZE_BYTES });
|
|
28
33
|
setGlobalDispatcher(getGlobalDispatcher().compose(interceptors.cache({ store })));
|
|
34
|
+
installed = true;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
//#endregion
|
|
@@ -806,6 +812,69 @@ function createSpawnWorkerTool(ctx, modelConfig) {
|
|
|
806
812
|
};
|
|
807
813
|
}
|
|
808
814
|
|
|
815
|
+
//#endregion
|
|
816
|
+
//#region src/tools/observe-pg-sync.ts
|
|
817
|
+
function asToolResult(value) {
|
|
818
|
+
return {
|
|
819
|
+
content: [{
|
|
820
|
+
type: `text`,
|
|
821
|
+
text: typeof value === `string` ? value : JSON.stringify(value, null, 2)
|
|
822
|
+
}],
|
|
823
|
+
details: {}
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
const PgSyncOperation = Type.Union([
|
|
827
|
+
Type.Literal(`insert`),
|
|
828
|
+
Type.Literal(`update`),
|
|
829
|
+
Type.Literal(`delete`)
|
|
830
|
+
]);
|
|
831
|
+
function createObservePgSyncTool(ctx) {
|
|
832
|
+
return {
|
|
833
|
+
name: `observe_pg_sync`,
|
|
834
|
+
label: `Observe Postgres Sync`,
|
|
835
|
+
description: `Observe an Electric Postgres shape stream and wake this agent when matching row changes arrive.`,
|
|
836
|
+
parameters: Type.Object({
|
|
837
|
+
url: Type.Optional(Type.String({ description: `Optional Electric shape endpoint URL. Defaults to the server-configured pg-sync URL.` })),
|
|
838
|
+
table: Type.String({
|
|
839
|
+
minLength: 1,
|
|
840
|
+
pattern: `\\S`,
|
|
841
|
+
description: `Postgres table name to observe.`
|
|
842
|
+
}),
|
|
843
|
+
columns: Type.Optional(Type.Array(Type.String(), { description: `Optional list of columns to include in the shape.` })),
|
|
844
|
+
where: Type.Optional(Type.String({ description: `Optional Electric shape WHERE clause.` })),
|
|
845
|
+
params: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Record(Type.String(), Type.String())])),
|
|
846
|
+
replica: Type.Optional(Type.Union([Type.Literal(`default`), Type.Literal(`full`)])),
|
|
847
|
+
wake: Type.Optional(Type.Object({
|
|
848
|
+
ops: Type.Optional(Type.Array(PgSyncOperation)),
|
|
849
|
+
debounceMs: Type.Optional(Type.Number())
|
|
850
|
+
}, { additionalProperties: false }))
|
|
851
|
+
}),
|
|
852
|
+
execute: async (_toolCallId, params) => {
|
|
853
|
+
const args = params;
|
|
854
|
+
if (typeof args.table !== `string` || args.table.trim().length === 0) throw new Error(`table is required`);
|
|
855
|
+
const source = pgSync({
|
|
856
|
+
url: args.url,
|
|
857
|
+
table: args.table,
|
|
858
|
+
columns: args.columns,
|
|
859
|
+
where: args.where,
|
|
860
|
+
params: args.params,
|
|
861
|
+
replica: args.replica
|
|
862
|
+
});
|
|
863
|
+
const wake = {
|
|
864
|
+
on: `change`,
|
|
865
|
+
...args.wake?.ops ? { ops: args.wake.ops } : {},
|
|
866
|
+
...args.wake?.debounceMs !== void 0 ? { debounceMs: args.wake.debounceMs } : {}
|
|
867
|
+
};
|
|
868
|
+
await ctx.observe(source, { wake });
|
|
869
|
+
return asToolResult({
|
|
870
|
+
sourceRef: source.sourceRef,
|
|
871
|
+
streamUrl: source.streamUrl,
|
|
872
|
+
wake
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
809
878
|
//#endregion
|
|
810
879
|
//#region src/tools/fork.ts
|
|
811
880
|
function createForkTool(ctx) {
|
|
@@ -860,6 +929,49 @@ Omit 'entityUrl' to fork your own session. Pass a different session's URL to for
|
|
|
860
929
|
};
|
|
861
930
|
}
|
|
862
931
|
|
|
932
|
+
//#endregion
|
|
933
|
+
//#region src/tools/set-title.ts
|
|
934
|
+
function createSetTitleTool(ctx) {
|
|
935
|
+
return {
|
|
936
|
+
name: `set_title`,
|
|
937
|
+
label: `Set Title`,
|
|
938
|
+
description: `Set the chat session title shown in the UI. Use this when the current title is missing, stale, misleading, or the user asks to rename the session. Provide a concise, human-readable title.`,
|
|
939
|
+
parameters: Type.Object({ title: Type.String({ description: `New session title. Whitespace is trimmed and the title must not be empty.` }) }),
|
|
940
|
+
execute: async (_toolCallId, params) => {
|
|
941
|
+
const { title } = params;
|
|
942
|
+
const trimmedTitle = typeof title === `string` ? title.trim() : ``;
|
|
943
|
+
if (trimmedTitle.length === 0) return {
|
|
944
|
+
content: [{
|
|
945
|
+
type: `text`,
|
|
946
|
+
text: `Error: title must be a non-empty string.`
|
|
947
|
+
}],
|
|
948
|
+
details: { updated: false }
|
|
949
|
+
};
|
|
950
|
+
try {
|
|
951
|
+
await ctx.setTag(`title`, trimmedTitle);
|
|
952
|
+
return {
|
|
953
|
+
content: [{
|
|
954
|
+
type: `text`,
|
|
955
|
+
text: `Session title set to “${trimmedTitle}”.`
|
|
956
|
+
}],
|
|
957
|
+
details: {
|
|
958
|
+
updated: true,
|
|
959
|
+
title: trimmedTitle
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
} catch (err) {
|
|
963
|
+
return {
|
|
964
|
+
content: [{
|
|
965
|
+
type: `text`,
|
|
966
|
+
text: `Error setting session title: ${err instanceof Error ? err.message : `Unknown error`}`
|
|
967
|
+
}],
|
|
968
|
+
details: { updated: false }
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
863
975
|
//#endregion
|
|
864
976
|
//#region src/model-catalog.ts
|
|
865
977
|
const MODEL_INPUTS_SCHEMA_DEF = `electricModelInputs`;
|
|
@@ -1041,7 +1153,7 @@ function modelInputSchemaDefs(catalog) {
|
|
|
1041
1153
|
|
|
1042
1154
|
//#endregion
|
|
1043
1155
|
//#region src/agents/horton.ts
|
|
1044
|
-
const TITLE_SYSTEM_PROMPT = "You generate concise chat session
|
|
1156
|
+
const TITLE_SYSTEM_PROMPT = "You generate a concise 3-5 word chat session title from the user's first message. Respond with only the title — no quotes, punctuation, preamble, or explanation. The user may reference images, files, or attachments you cannot see; infer a title from their intent anyway. Never apologize or say anything is missing — always output a short title.";
|
|
1045
1157
|
const TITLE_USER_PROMPT = (userMessage) => `User request:\n${userMessage}`;
|
|
1046
1158
|
const TITLE_GENERATION_TIMEOUT_MS = 8e3;
|
|
1047
1159
|
const HORTON_SKILLS_SLASH_COMMAND_OWNER = `horton:skills`;
|
|
@@ -1135,12 +1247,16 @@ function withTimeout(promise, ms, description) {
|
|
|
1135
1247
|
if (timeout) clearTimeout(timeout);
|
|
1136
1248
|
});
|
|
1137
1249
|
}
|
|
1250
|
+
function looksLikeNonTitle(title) {
|
|
1251
|
+
if (title.split(/\s+/).filter(Boolean).length > 8) return true;
|
|
1252
|
+
return /[!?,]/.test(title);
|
|
1253
|
+
}
|
|
1138
1254
|
async function generateTitle(userMessage, llmCall, onFallback) {
|
|
1139
1255
|
try {
|
|
1140
1256
|
const raw = await llmCall(TITLE_USER_PROMPT(userMessage));
|
|
1141
1257
|
const title = raw.trim();
|
|
1142
|
-
if (title.length > 0) return title;
|
|
1143
|
-
onFallback?.(`empty LLM title response`);
|
|
1258
|
+
if (title.length > 0 && !looksLikeNonTitle(title)) return title;
|
|
1259
|
+
onFallback?.(title.length === 0 ? `empty LLM title response` : `non-title LLM response`);
|
|
1144
1260
|
return buildFallbackTitle(userMessage);
|
|
1145
1261
|
} catch (err) {
|
|
1146
1262
|
onFallback?.(err instanceof Error ? err.message : String(err));
|
|
@@ -1150,6 +1266,7 @@ async function generateTitle(userMessage, llmCall, onFallback) {
|
|
|
1150
1266
|
function buildHortonSystemPrompt(workingDirectory, opts = {}) {
|
|
1151
1267
|
const docsTools = opts.hasDocsSupport ? `\n- search_electric_agents_docs: hybrid search over the built-in Electric Agents docs index` : ``;
|
|
1152
1268
|
const eventSourceTools = opts.hasEventSourceTools ? `\n- list_event_sources: list external webhook/event feeds you can subscribe to, including available buckets and parameters\n- subscribe_event_source: subscribe yourself to one of those feeds or buckets so matching future events wake you\n- list_event_source_subscriptions: list your active event source subscriptions\n- unsubscribe_event_source: remove one of your event source subscriptions by id` : ``;
|
|
1269
|
+
const titleTool = `\n- set_title: set or rename this chat session's UI title`;
|
|
1153
1270
|
const scheduleTools = opts.hasScheduleTools ? `\n- upsert_cron_schedule: create or update a recurring cron wake for yourself. Always include payload with the concrete instruction/message you should receive when the cron fires.\n- delete_schedule: delete one of your cron or future-send schedules by stable id\n- list_schedules: list your manifest-backed cron and future-send schedules` : ``;
|
|
1154
1271
|
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` : ``;
|
|
1155
1272
|
const docsGuidance = opts.hasDocsSupport ? `\n- For ANY question about Electric Agents or this framework, ALWAYS use search_electric_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.` : ``;
|
|
@@ -1205,8 +1322,9 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
|
|
|
1205
1322
|
- fetch_url: fetch and convert a URL to markdown
|
|
1206
1323
|
- spawn_worker: dispatch a subagent for an isolated task
|
|
1207
1324
|
- fork: spawn a child session that inherits this conversation's history up to the latest completed response. Same parent-ownership model as spawn_worker — when the fork's next run finishes, you'll wake with its response.
|
|
1325
|
+
- observe_pg_sync: observe an Electric Postgres sync stream and wake on matching changes
|
|
1208
1326
|
- send: send a message to an Electric Agent/entity. To schedule future work for yourself, call send with self: true and afterMs.
|
|
1209
|
-
${eventSourceTools}${scheduleTools}${docsTools}${skillsTools}
|
|
1327
|
+
${eventSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
|
|
1210
1328
|
|
|
1211
1329
|
# Working with files
|
|
1212
1330
|
- Prefer edit over write when modifying existing files.
|
|
@@ -1273,6 +1391,8 @@ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
|
|
|
1273
1391
|
})] : [createFetchUrlTool(sandbox)],
|
|
1274
1392
|
createSpawnWorkerTool(ctx, opts.modelConfig),
|
|
1275
1393
|
createForkTool(ctx),
|
|
1394
|
+
createObservePgSyncTool(ctx),
|
|
1395
|
+
createSetTitleTool(ctx),
|
|
1276
1396
|
createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }),
|
|
1277
1397
|
...opts.docsSearchTool ? [opts.docsSearchTool] : []
|
|
1278
1398
|
];
|
package/dist/index.cjs
CHANGED
|
@@ -818,6 +818,69 @@ function createSpawnWorkerTool(ctx, modelConfig) {
|
|
|
818
818
|
};
|
|
819
819
|
}
|
|
820
820
|
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region src/tools/observe-pg-sync.ts
|
|
823
|
+
function asToolResult(value) {
|
|
824
|
+
return {
|
|
825
|
+
content: [{
|
|
826
|
+
type: `text`,
|
|
827
|
+
text: typeof value === `string` ? value : JSON.stringify(value, null, 2)
|
|
828
|
+
}],
|
|
829
|
+
details: {}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
const PgSyncOperation = __sinclair_typebox.Type.Union([
|
|
833
|
+
__sinclair_typebox.Type.Literal(`insert`),
|
|
834
|
+
__sinclair_typebox.Type.Literal(`update`),
|
|
835
|
+
__sinclair_typebox.Type.Literal(`delete`)
|
|
836
|
+
]);
|
|
837
|
+
function createObservePgSyncTool(ctx) {
|
|
838
|
+
return {
|
|
839
|
+
name: `observe_pg_sync`,
|
|
840
|
+
label: `Observe Postgres Sync`,
|
|
841
|
+
description: `Observe an Electric Postgres shape stream and wake this agent when matching row changes arrive.`,
|
|
842
|
+
parameters: __sinclair_typebox.Type.Object({
|
|
843
|
+
url: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String({ description: `Optional Electric shape endpoint URL. Defaults to the server-configured pg-sync URL.` })),
|
|
844
|
+
table: __sinclair_typebox.Type.String({
|
|
845
|
+
minLength: 1,
|
|
846
|
+
pattern: `\\S`,
|
|
847
|
+
description: `Postgres table name to observe.`
|
|
848
|
+
}),
|
|
849
|
+
columns: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(__sinclair_typebox.Type.String(), { description: `Optional list of columns to include in the shape.` })),
|
|
850
|
+
where: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String({ description: `Optional Electric shape WHERE clause.` })),
|
|
851
|
+
params: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Array(__sinclair_typebox.Type.String()), __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String())])),
|
|
852
|
+
replica: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`default`), __sinclair_typebox.Type.Literal(`full`)])),
|
|
853
|
+
wake: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Object({
|
|
854
|
+
ops: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(PgSyncOperation)),
|
|
855
|
+
debounceMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number())
|
|
856
|
+
}, { additionalProperties: false }))
|
|
857
|
+
}),
|
|
858
|
+
execute: async (_toolCallId, params) => {
|
|
859
|
+
const args = params;
|
|
860
|
+
if (typeof args.table !== `string` || args.table.trim().length === 0) throw new Error(`table is required`);
|
|
861
|
+
const source = (0, __electric_ax_agents_runtime.pgSync)({
|
|
862
|
+
url: args.url,
|
|
863
|
+
table: args.table,
|
|
864
|
+
columns: args.columns,
|
|
865
|
+
where: args.where,
|
|
866
|
+
params: args.params,
|
|
867
|
+
replica: args.replica
|
|
868
|
+
});
|
|
869
|
+
const wake = {
|
|
870
|
+
on: `change`,
|
|
871
|
+
...args.wake?.ops ? { ops: args.wake.ops } : {},
|
|
872
|
+
...args.wake?.debounceMs !== void 0 ? { debounceMs: args.wake.debounceMs } : {}
|
|
873
|
+
};
|
|
874
|
+
await ctx.observe(source, { wake });
|
|
875
|
+
return asToolResult({
|
|
876
|
+
sourceRef: source.sourceRef,
|
|
877
|
+
streamUrl: source.streamUrl,
|
|
878
|
+
wake
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
821
884
|
//#endregion
|
|
822
885
|
//#region src/tools/fork.ts
|
|
823
886
|
function createForkTool(ctx) {
|
|
@@ -872,6 +935,49 @@ Omit 'entityUrl' to fork your own session. Pass a different session's URL to for
|
|
|
872
935
|
};
|
|
873
936
|
}
|
|
874
937
|
|
|
938
|
+
//#endregion
|
|
939
|
+
//#region src/tools/set-title.ts
|
|
940
|
+
function createSetTitleTool(ctx) {
|
|
941
|
+
return {
|
|
942
|
+
name: `set_title`,
|
|
943
|
+
label: `Set Title`,
|
|
944
|
+
description: `Set the chat session title shown in the UI. Use this when the current title is missing, stale, misleading, or the user asks to rename the session. Provide a concise, human-readable title.`,
|
|
945
|
+
parameters: __sinclair_typebox.Type.Object({ title: __sinclair_typebox.Type.String({ description: `New session title. Whitespace is trimmed and the title must not be empty.` }) }),
|
|
946
|
+
execute: async (_toolCallId, params) => {
|
|
947
|
+
const { title } = params;
|
|
948
|
+
const trimmedTitle = typeof title === `string` ? title.trim() : ``;
|
|
949
|
+
if (trimmedTitle.length === 0) return {
|
|
950
|
+
content: [{
|
|
951
|
+
type: `text`,
|
|
952
|
+
text: `Error: title must be a non-empty string.`
|
|
953
|
+
}],
|
|
954
|
+
details: { updated: false }
|
|
955
|
+
};
|
|
956
|
+
try {
|
|
957
|
+
await ctx.setTag(`title`, trimmedTitle);
|
|
958
|
+
return {
|
|
959
|
+
content: [{
|
|
960
|
+
type: `text`,
|
|
961
|
+
text: `Session title set to “${trimmedTitle}”.`
|
|
962
|
+
}],
|
|
963
|
+
details: {
|
|
964
|
+
updated: true,
|
|
965
|
+
title: trimmedTitle
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
} catch (err) {
|
|
969
|
+
return {
|
|
970
|
+
content: [{
|
|
971
|
+
type: `text`,
|
|
972
|
+
text: `Error setting session title: ${err instanceof Error ? err.message : `Unknown error`}`
|
|
973
|
+
}],
|
|
974
|
+
details: { updated: false }
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
875
981
|
//#endregion
|
|
876
982
|
//#region src/model-catalog.ts
|
|
877
983
|
const MODEL_INPUTS_SCHEMA_DEF = `electricModelInputs`;
|
|
@@ -1054,7 +1160,7 @@ function modelInputSchemaDefs(catalog) {
|
|
|
1054
1160
|
//#endregion
|
|
1055
1161
|
//#region src/agents/horton.ts
|
|
1056
1162
|
const HORTON_MODEL = `claude-sonnet-4-6`;
|
|
1057
|
-
const TITLE_SYSTEM_PROMPT = "You generate concise chat session
|
|
1163
|
+
const TITLE_SYSTEM_PROMPT = "You generate a concise 3-5 word chat session title from the user's first message. Respond with only the title — no quotes, punctuation, preamble, or explanation. The user may reference images, files, or attachments you cannot see; infer a title from their intent anyway. Never apologize or say anything is missing — always output a short title.";
|
|
1058
1164
|
const TITLE_USER_PROMPT = (userMessage) => `User request:\n${userMessage}`;
|
|
1059
1165
|
const TITLE_GENERATION_TIMEOUT_MS = 8e3;
|
|
1060
1166
|
const HORTON_SKILLS_SLASH_COMMAND_OWNER = `horton:skills`;
|
|
@@ -1148,12 +1254,16 @@ function withTimeout(promise, ms, description) {
|
|
|
1148
1254
|
if (timeout) clearTimeout(timeout);
|
|
1149
1255
|
});
|
|
1150
1256
|
}
|
|
1257
|
+
function looksLikeNonTitle(title) {
|
|
1258
|
+
if (title.split(/\s+/).filter(Boolean).length > 8) return true;
|
|
1259
|
+
return /[!?,]/.test(title);
|
|
1260
|
+
}
|
|
1151
1261
|
async function generateTitle(userMessage, llmCall, onFallback) {
|
|
1152
1262
|
try {
|
|
1153
1263
|
const raw = await llmCall(TITLE_USER_PROMPT(userMessage));
|
|
1154
1264
|
const title = raw.trim();
|
|
1155
|
-
if (title.length > 0) return title;
|
|
1156
|
-
onFallback?.(`empty LLM title response`);
|
|
1265
|
+
if (title.length > 0 && !looksLikeNonTitle(title)) return title;
|
|
1266
|
+
onFallback?.(title.length === 0 ? `empty LLM title response` : `non-title LLM response`);
|
|
1157
1267
|
return buildFallbackTitle(userMessage);
|
|
1158
1268
|
} catch (err) {
|
|
1159
1269
|
onFallback?.(err instanceof Error ? err.message : String(err));
|
|
@@ -1163,6 +1273,7 @@ async function generateTitle(userMessage, llmCall, onFallback) {
|
|
|
1163
1273
|
function buildHortonSystemPrompt(workingDirectory, opts = {}) {
|
|
1164
1274
|
const docsTools = opts.hasDocsSupport ? `\n- search_electric_agents_docs: hybrid search over the built-in Electric Agents docs index` : ``;
|
|
1165
1275
|
const eventSourceTools = opts.hasEventSourceTools ? `\n- list_event_sources: list external webhook/event feeds you can subscribe to, including available buckets and parameters\n- subscribe_event_source: subscribe yourself to one of those feeds or buckets so matching future events wake you\n- list_event_source_subscriptions: list your active event source subscriptions\n- unsubscribe_event_source: remove one of your event source subscriptions by id` : ``;
|
|
1276
|
+
const titleTool = `\n- set_title: set or rename this chat session's UI title`;
|
|
1166
1277
|
const scheduleTools = opts.hasScheduleTools ? `\n- upsert_cron_schedule: create or update a recurring cron wake for yourself. Always include payload with the concrete instruction/message you should receive when the cron fires.\n- delete_schedule: delete one of your cron or future-send schedules by stable id\n- list_schedules: list your manifest-backed cron and future-send schedules` : ``;
|
|
1167
1278
|
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` : ``;
|
|
1168
1279
|
const docsGuidance = opts.hasDocsSupport ? `\n- For ANY question about Electric Agents or this framework, ALWAYS use search_electric_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.` : ``;
|
|
@@ -1218,8 +1329,9 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
|
|
|
1218
1329
|
- fetch_url: fetch and convert a URL to markdown
|
|
1219
1330
|
- spawn_worker: dispatch a subagent for an isolated task
|
|
1220
1331
|
- fork: spawn a child session that inherits this conversation's history up to the latest completed response. Same parent-ownership model as spawn_worker — when the fork's next run finishes, you'll wake with its response.
|
|
1332
|
+
- observe_pg_sync: observe an Electric Postgres sync stream and wake on matching changes
|
|
1221
1333
|
- send: send a message to an Electric Agent/entity. To schedule future work for yourself, call send with self: true and afterMs.
|
|
1222
|
-
${eventSourceTools}${scheduleTools}${docsTools}${skillsTools}
|
|
1334
|
+
${eventSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
|
|
1223
1335
|
|
|
1224
1336
|
# Working with files
|
|
1225
1337
|
- Prefer edit over write when modifying existing files.
|
|
@@ -1286,6 +1398,8 @@ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
|
|
|
1286
1398
|
})] : [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox)],
|
|
1287
1399
|
createSpawnWorkerTool(ctx, opts.modelConfig),
|
|
1288
1400
|
createForkTool(ctx),
|
|
1401
|
+
createObservePgSyncTool(ctx),
|
|
1402
|
+
createSetTitleTool(ctx),
|
|
1289
1403
|
(0, __electric_ax_agents_runtime_tools.createSendTool)(ctx.send, { selfEntityUrl: ctx.entityUrl }),
|
|
1290
1404
|
...opts.docsSearchTool ? [opts.docsSearchTool] : []
|
|
1291
1405
|
];
|
|
@@ -1912,13 +2026,19 @@ function resolveCwd(args, fallback) {
|
|
|
1912
2026
|
//#endregion
|
|
1913
2027
|
//#region src/durable-streams-cache.ts
|
|
1914
2028
|
const MEMORY_CACHE_SIZE_BYTES = 100 * 1024 * 1024;
|
|
2029
|
+
let installed = false;
|
|
1915
2030
|
function installDurableStreamsFetchCache(options = {}) {
|
|
1916
2031
|
if (options === false) return;
|
|
2032
|
+
if (installed) {
|
|
2033
|
+
console.warn(`[agents] installDurableStreamsFetchCache called more than once; ignoring`);
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
1917
2036
|
const store = options.store === `sqlite` || options.sqliteLocation ? new undici.cacheStores.SqliteCacheStore({
|
|
1918
2037
|
location: options.sqliteLocation,
|
|
1919
2038
|
maxCount: options.maxCount
|
|
1920
2039
|
}) : new undici.cacheStores.MemoryCacheStore({ maxSize: MEMORY_CACHE_SIZE_BYTES });
|
|
1921
2040
|
(0, undici.setGlobalDispatcher)((0, undici.getGlobalDispatcher)().compose(undici.interceptors.cache({ store })));
|
|
2041
|
+
installed = true;
|
|
1922
2042
|
}
|
|
1923
2043
|
|
|
1924
2044
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mergeElectricPrincipalHeader } from "./server-headers-KD5yHFYT.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, buildSkillSlashCommands, completeWithLowCostModel, createContextSkillLoader, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModel, getMoonshotModels, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
4
|
+
import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, buildSkillSlashCommands, completeWithLowCostModel, createContextSkillLoader, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModel, getMoonshotModels, pgSync, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
5
5
|
import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createScheduleTools, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
|
|
6
6
|
import { chooseDefaultSandbox, isE2BAvailable, lazySandbox, remoteSandbox } from "@electric-ax/agents-runtime/sandbox";
|
|
7
7
|
import fsSync from "node:fs";
|
|
@@ -794,6 +794,69 @@ function createSpawnWorkerTool(ctx, modelConfig) {
|
|
|
794
794
|
};
|
|
795
795
|
}
|
|
796
796
|
|
|
797
|
+
//#endregion
|
|
798
|
+
//#region src/tools/observe-pg-sync.ts
|
|
799
|
+
function asToolResult(value) {
|
|
800
|
+
return {
|
|
801
|
+
content: [{
|
|
802
|
+
type: `text`,
|
|
803
|
+
text: typeof value === `string` ? value : JSON.stringify(value, null, 2)
|
|
804
|
+
}],
|
|
805
|
+
details: {}
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
const PgSyncOperation = Type.Union([
|
|
809
|
+
Type.Literal(`insert`),
|
|
810
|
+
Type.Literal(`update`),
|
|
811
|
+
Type.Literal(`delete`)
|
|
812
|
+
]);
|
|
813
|
+
function createObservePgSyncTool(ctx) {
|
|
814
|
+
return {
|
|
815
|
+
name: `observe_pg_sync`,
|
|
816
|
+
label: `Observe Postgres Sync`,
|
|
817
|
+
description: `Observe an Electric Postgres shape stream and wake this agent when matching row changes arrive.`,
|
|
818
|
+
parameters: Type.Object({
|
|
819
|
+
url: Type.Optional(Type.String({ description: `Optional Electric shape endpoint URL. Defaults to the server-configured pg-sync URL.` })),
|
|
820
|
+
table: Type.String({
|
|
821
|
+
minLength: 1,
|
|
822
|
+
pattern: `\\S`,
|
|
823
|
+
description: `Postgres table name to observe.`
|
|
824
|
+
}),
|
|
825
|
+
columns: Type.Optional(Type.Array(Type.String(), { description: `Optional list of columns to include in the shape.` })),
|
|
826
|
+
where: Type.Optional(Type.String({ description: `Optional Electric shape WHERE clause.` })),
|
|
827
|
+
params: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Record(Type.String(), Type.String())])),
|
|
828
|
+
replica: Type.Optional(Type.Union([Type.Literal(`default`), Type.Literal(`full`)])),
|
|
829
|
+
wake: Type.Optional(Type.Object({
|
|
830
|
+
ops: Type.Optional(Type.Array(PgSyncOperation)),
|
|
831
|
+
debounceMs: Type.Optional(Type.Number())
|
|
832
|
+
}, { additionalProperties: false }))
|
|
833
|
+
}),
|
|
834
|
+
execute: async (_toolCallId, params) => {
|
|
835
|
+
const args = params;
|
|
836
|
+
if (typeof args.table !== `string` || args.table.trim().length === 0) throw new Error(`table is required`);
|
|
837
|
+
const source = pgSync({
|
|
838
|
+
url: args.url,
|
|
839
|
+
table: args.table,
|
|
840
|
+
columns: args.columns,
|
|
841
|
+
where: args.where,
|
|
842
|
+
params: args.params,
|
|
843
|
+
replica: args.replica
|
|
844
|
+
});
|
|
845
|
+
const wake = {
|
|
846
|
+
on: `change`,
|
|
847
|
+
...args.wake?.ops ? { ops: args.wake.ops } : {},
|
|
848
|
+
...args.wake?.debounceMs !== void 0 ? { debounceMs: args.wake.debounceMs } : {}
|
|
849
|
+
};
|
|
850
|
+
await ctx.observe(source, { wake });
|
|
851
|
+
return asToolResult({
|
|
852
|
+
sourceRef: source.sourceRef,
|
|
853
|
+
streamUrl: source.streamUrl,
|
|
854
|
+
wake
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
797
860
|
//#endregion
|
|
798
861
|
//#region src/tools/fork.ts
|
|
799
862
|
function createForkTool(ctx) {
|
|
@@ -848,6 +911,49 @@ Omit 'entityUrl' to fork your own session. Pass a different session's URL to for
|
|
|
848
911
|
};
|
|
849
912
|
}
|
|
850
913
|
|
|
914
|
+
//#endregion
|
|
915
|
+
//#region src/tools/set-title.ts
|
|
916
|
+
function createSetTitleTool(ctx) {
|
|
917
|
+
return {
|
|
918
|
+
name: `set_title`,
|
|
919
|
+
label: `Set Title`,
|
|
920
|
+
description: `Set the chat session title shown in the UI. Use this when the current title is missing, stale, misleading, or the user asks to rename the session. Provide a concise, human-readable title.`,
|
|
921
|
+
parameters: Type.Object({ title: Type.String({ description: `New session title. Whitespace is trimmed and the title must not be empty.` }) }),
|
|
922
|
+
execute: async (_toolCallId, params) => {
|
|
923
|
+
const { title } = params;
|
|
924
|
+
const trimmedTitle = typeof title === `string` ? title.trim() : ``;
|
|
925
|
+
if (trimmedTitle.length === 0) return {
|
|
926
|
+
content: [{
|
|
927
|
+
type: `text`,
|
|
928
|
+
text: `Error: title must be a non-empty string.`
|
|
929
|
+
}],
|
|
930
|
+
details: { updated: false }
|
|
931
|
+
};
|
|
932
|
+
try {
|
|
933
|
+
await ctx.setTag(`title`, trimmedTitle);
|
|
934
|
+
return {
|
|
935
|
+
content: [{
|
|
936
|
+
type: `text`,
|
|
937
|
+
text: `Session title set to “${trimmedTitle}”.`
|
|
938
|
+
}],
|
|
939
|
+
details: {
|
|
940
|
+
updated: true,
|
|
941
|
+
title: trimmedTitle
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
} catch (err) {
|
|
945
|
+
return {
|
|
946
|
+
content: [{
|
|
947
|
+
type: `text`,
|
|
948
|
+
text: `Error setting session title: ${err instanceof Error ? err.message : `Unknown error`}`
|
|
949
|
+
}],
|
|
950
|
+
details: { updated: false }
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
851
957
|
//#endregion
|
|
852
958
|
//#region src/model-catalog.ts
|
|
853
959
|
const MODEL_INPUTS_SCHEMA_DEF = `electricModelInputs`;
|
|
@@ -1030,7 +1136,7 @@ function modelInputSchemaDefs(catalog) {
|
|
|
1030
1136
|
//#endregion
|
|
1031
1137
|
//#region src/agents/horton.ts
|
|
1032
1138
|
const HORTON_MODEL = `claude-sonnet-4-6`;
|
|
1033
|
-
const TITLE_SYSTEM_PROMPT = "You generate concise chat session
|
|
1139
|
+
const TITLE_SYSTEM_PROMPT = "You generate a concise 3-5 word chat session title from the user's first message. Respond with only the title — no quotes, punctuation, preamble, or explanation. The user may reference images, files, or attachments you cannot see; infer a title from their intent anyway. Never apologize or say anything is missing — always output a short title.";
|
|
1034
1140
|
const TITLE_USER_PROMPT = (userMessage) => `User request:\n${userMessage}`;
|
|
1035
1141
|
const TITLE_GENERATION_TIMEOUT_MS = 8e3;
|
|
1036
1142
|
const HORTON_SKILLS_SLASH_COMMAND_OWNER = `horton:skills`;
|
|
@@ -1124,12 +1230,16 @@ function withTimeout(promise, ms, description) {
|
|
|
1124
1230
|
if (timeout) clearTimeout(timeout);
|
|
1125
1231
|
});
|
|
1126
1232
|
}
|
|
1233
|
+
function looksLikeNonTitle(title) {
|
|
1234
|
+
if (title.split(/\s+/).filter(Boolean).length > 8) return true;
|
|
1235
|
+
return /[!?,]/.test(title);
|
|
1236
|
+
}
|
|
1127
1237
|
async function generateTitle(userMessage, llmCall, onFallback) {
|
|
1128
1238
|
try {
|
|
1129
1239
|
const raw = await llmCall(TITLE_USER_PROMPT(userMessage));
|
|
1130
1240
|
const title = raw.trim();
|
|
1131
|
-
if (title.length > 0) return title;
|
|
1132
|
-
onFallback?.(`empty LLM title response`);
|
|
1241
|
+
if (title.length > 0 && !looksLikeNonTitle(title)) return title;
|
|
1242
|
+
onFallback?.(title.length === 0 ? `empty LLM title response` : `non-title LLM response`);
|
|
1133
1243
|
return buildFallbackTitle(userMessage);
|
|
1134
1244
|
} catch (err) {
|
|
1135
1245
|
onFallback?.(err instanceof Error ? err.message : String(err));
|
|
@@ -1139,6 +1249,7 @@ async function generateTitle(userMessage, llmCall, onFallback) {
|
|
|
1139
1249
|
function buildHortonSystemPrompt(workingDirectory, opts = {}) {
|
|
1140
1250
|
const docsTools = opts.hasDocsSupport ? `\n- search_electric_agents_docs: hybrid search over the built-in Electric Agents docs index` : ``;
|
|
1141
1251
|
const eventSourceTools = opts.hasEventSourceTools ? `\n- list_event_sources: list external webhook/event feeds you can subscribe to, including available buckets and parameters\n- subscribe_event_source: subscribe yourself to one of those feeds or buckets so matching future events wake you\n- list_event_source_subscriptions: list your active event source subscriptions\n- unsubscribe_event_source: remove one of your event source subscriptions by id` : ``;
|
|
1252
|
+
const titleTool = `\n- set_title: set or rename this chat session's UI title`;
|
|
1142
1253
|
const scheduleTools = opts.hasScheduleTools ? `\n- upsert_cron_schedule: create or update a recurring cron wake for yourself. Always include payload with the concrete instruction/message you should receive when the cron fires.\n- delete_schedule: delete one of your cron or future-send schedules by stable id\n- list_schedules: list your manifest-backed cron and future-send schedules` : ``;
|
|
1143
1254
|
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` : ``;
|
|
1144
1255
|
const docsGuidance = opts.hasDocsSupport ? `\n- For ANY question about Electric Agents or this framework, ALWAYS use search_electric_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.` : ``;
|
|
@@ -1194,8 +1305,9 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
|
|
|
1194
1305
|
- fetch_url: fetch and convert a URL to markdown
|
|
1195
1306
|
- spawn_worker: dispatch a subagent for an isolated task
|
|
1196
1307
|
- fork: spawn a child session that inherits this conversation's history up to the latest completed response. Same parent-ownership model as spawn_worker — when the fork's next run finishes, you'll wake with its response.
|
|
1308
|
+
- observe_pg_sync: observe an Electric Postgres sync stream and wake on matching changes
|
|
1197
1309
|
- send: send a message to an Electric Agent/entity. To schedule future work for yourself, call send with self: true and afterMs.
|
|
1198
|
-
${eventSourceTools}${scheduleTools}${docsTools}${skillsTools}
|
|
1310
|
+
${eventSourceTools}${titleTool}${scheduleTools}${docsTools}${skillsTools}
|
|
1199
1311
|
|
|
1200
1312
|
# Working with files
|
|
1201
1313
|
- Prefer edit over write when modifying existing files.
|
|
@@ -1262,6 +1374,8 @@ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
|
|
|
1262
1374
|
})] : [createFetchUrlTool(sandbox)],
|
|
1263
1375
|
createSpawnWorkerTool(ctx, opts.modelConfig),
|
|
1264
1376
|
createForkTool(ctx),
|
|
1377
|
+
createObservePgSyncTool(ctx),
|
|
1378
|
+
createSetTitleTool(ctx),
|
|
1265
1379
|
createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }),
|
|
1266
1380
|
...opts.docsSearchTool ? [opts.docsSearchTool] : []
|
|
1267
1381
|
];
|
|
@@ -1888,13 +2002,19 @@ function resolveCwd(args, fallback) {
|
|
|
1888
2002
|
//#endregion
|
|
1889
2003
|
//#region src/durable-streams-cache.ts
|
|
1890
2004
|
const MEMORY_CACHE_SIZE_BYTES = 100 * 1024 * 1024;
|
|
2005
|
+
let installed = false;
|
|
1891
2006
|
function installDurableStreamsFetchCache(options = {}) {
|
|
1892
2007
|
if (options === false) return;
|
|
2008
|
+
if (installed) {
|
|
2009
|
+
console.warn(`[agents] installDurableStreamsFetchCache called more than once; ignoring`);
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
1893
2012
|
const store = options.store === `sqlite` || options.sqliteLocation ? new cacheStores.SqliteCacheStore({
|
|
1894
2013
|
location: options.sqliteLocation,
|
|
1895
2014
|
maxCount: options.maxCount
|
|
1896
2015
|
}) : new cacheStores.MemoryCacheStore({ maxSize: MEMORY_CACHE_SIZE_BYTES });
|
|
1897
2016
|
setGlobalDispatcher(getGlobalDispatcher().compose(interceptors.cache({ store })));
|
|
2017
|
+
installed = true;
|
|
1898
2018
|
}
|
|
1899
2019
|
|
|
1900
2020
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-ax/agents",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.17",
|
|
4
4
|
"description": "Built-in Electric Agents runtimes such as Horton and worker",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"sqlite-vec": "^0.1.9",
|
|
50
50
|
"undici": "^7.24.7",
|
|
51
51
|
"zod": "^4.3.6",
|
|
52
|
-
"@electric-ax/agents-mcp": "0.2.
|
|
53
|
-
"@electric-ax/agents-runtime": "0.3.
|
|
52
|
+
"@electric-ax/agents-mcp": "0.2.3",
|
|
53
|
+
"@electric-ax/agents-runtime": "0.3.13"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/better-sqlite3": "^7.6.13",
|