@qearlyao/familiar 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +31 -0
- package/HEARTBEAT.md +23 -0
- package/LICENSE +21 -0
- package/MEMORY.md +1 -0
- package/README.md +245 -0
- package/SOUL.md +13 -0
- package/USER.md +13 -0
- package/config.example.toml +221 -0
- package/dist/agent-events.js +167 -0
- package/dist/agent.js +590 -0
- package/dist/browser-tools.js +638 -0
- package/dist/chat-log.js +130 -0
- package/dist/cli.js +168 -0
- package/dist/config.js +804 -0
- package/dist/data-retention.js +54 -0
- package/dist/discord.js +1203 -0
- package/dist/generated-media.js +86 -0
- package/dist/image-derivatives.js +102 -0
- package/dist/image-gen.js +440 -0
- package/dist/inbound-attachments.js +266 -0
- package/dist/index.js +10 -0
- package/dist/media-understanding.js +120 -0
- package/dist/memory/diary/ambient-injector.js +180 -0
- package/dist/memory/diary/ambient.js +124 -0
- package/dist/memory/diary/chunks.js +231 -0
- package/dist/memory/diary/index.js +3 -0
- package/dist/memory/diary/indexer.js +93 -0
- package/dist/memory/doctor.js +250 -0
- package/dist/memory/index/chunk-indexer.js +151 -0
- package/dist/memory/index/embedding-provider.js +119 -0
- package/dist/memory/index/fts-query.js +18 -0
- package/dist/memory/index/retrieval.js +246 -0
- package/dist/memory/index/schema.js +157 -0
- package/dist/memory/index/store.js +513 -0
- package/dist/memory/index/vec.js +72 -0
- package/dist/memory/index/vector-codec.js +27 -0
- package/dist/memory/lcm/backfill.js +247 -0
- package/dist/memory/lcm/condense.js +146 -0
- package/dist/memory/lcm/context-transformer.js +662 -0
- package/dist/memory/lcm/context.js +421 -0
- package/dist/memory/lcm/eviction-score.js +38 -0
- package/dist/memory/lcm/index.js +6 -0
- package/dist/memory/lcm/indexer.js +200 -0
- package/dist/memory/lcm/normalize.js +235 -0
- package/dist/memory/lcm/schema.js +188 -0
- package/dist/memory/lcm/segment-manager.js +136 -0
- package/dist/memory/lcm/store.js +722 -0
- package/dist/memory/lcm/summarizer.js +258 -0
- package/dist/memory/lcm/types.js +1 -0
- package/dist/memory/operator.js +477 -0
- package/dist/memory/service.js +202 -0
- package/dist/memory/tools.js +205 -0
- package/dist/models.js +165 -0
- package/dist/persona.js +54 -0
- package/dist/runtime.js +493 -0
- package/dist/scheduler.js +200 -0
- package/dist/settings.js +116 -0
- package/dist/skills.js +38 -0
- package/dist/tts.js +143 -0
- package/dist/web-auth.js +105 -0
- package/dist/web-events.js +114 -0
- package/dist/web-http.js +29 -0
- package/dist/web-static.js +106 -0
- package/dist/web-tools.js +940 -0
- package/dist/web-types.js +2 -0
- package/dist/web.js +844 -0
- package/package.json +60 -0
- package/web/dist/assets/index-ClgkMgaq.css +2 -0
- package/web/dist/assets/index-Cu2QquuR.js +59 -0
- package/web/dist/favicon.svg +1 -0
- package/web/dist/icons.svg +24 -0
- package/web/dist/index.html +20 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { parse } from "smol-toml";
|
|
5
|
+
const loggedConfigWarnings = new Set();
|
|
6
|
+
const DEFAULT_MEMORY_EMBEDDING_BASE_URLS = {
|
|
7
|
+
google: "https://generativelanguage.googleapis.com/v1beta",
|
|
8
|
+
};
|
|
9
|
+
const DEFAULT_MEMORY_EMBEDDING_API_KEY_ENVS = {
|
|
10
|
+
google: "GEMINI_API_KEY",
|
|
11
|
+
};
|
|
12
|
+
function interpolateEnv(value) {
|
|
13
|
+
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (_match, name, fallback) => {
|
|
14
|
+
return process.env[name] ?? fallback ?? "";
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function interpolateValue(value) {
|
|
18
|
+
if (typeof value === "string")
|
|
19
|
+
return interpolateEnv(value);
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return value.map(interpolateValue);
|
|
22
|
+
if (value && typeof value === "object") {
|
|
23
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, interpolateValue(child)]));
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function readString(value, path) {
|
|
28
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
29
|
+
throw new Error(`Missing required config value: ${path}`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function readOptionalString(value, fallback) {
|
|
34
|
+
return typeof value === "string" && value.trim() !== "" ? value : fallback;
|
|
35
|
+
}
|
|
36
|
+
function readOptionalConfigString(value, path) {
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (typeof value !== "string")
|
|
40
|
+
throw new Error(`Config value ${path} must be a string`);
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
return trimmed ? trimmed : undefined;
|
|
43
|
+
}
|
|
44
|
+
function readConfigString(value, fallback, path) {
|
|
45
|
+
const read = readOptionalConfigString(value, path);
|
|
46
|
+
return read ?? fallback;
|
|
47
|
+
}
|
|
48
|
+
function readStringArray(value, path) {
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
return [];
|
|
51
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
52
|
+
throw new Error(`Config value must be a string array: ${path}`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function readStringRecord(value, path) {
|
|
57
|
+
if (value === undefined)
|
|
58
|
+
return {};
|
|
59
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
60
|
+
throw new Error(`Config value must be a string map: ${path}`);
|
|
61
|
+
}
|
|
62
|
+
const entries = Object.entries(value);
|
|
63
|
+
for (const [key, child] of entries) {
|
|
64
|
+
if (typeof child !== "string")
|
|
65
|
+
throw new Error(`Config value must be a string map: ${path}.${key}`);
|
|
66
|
+
}
|
|
67
|
+
return Object.fromEntries(entries);
|
|
68
|
+
}
|
|
69
|
+
function warnOnce(key, message) {
|
|
70
|
+
if (loggedConfigWarnings.has(key))
|
|
71
|
+
return;
|
|
72
|
+
loggedConfigWarnings.add(key);
|
|
73
|
+
console.warn(message);
|
|
74
|
+
}
|
|
75
|
+
function readCacheRetention(value, path = "agent.cache_retention") {
|
|
76
|
+
if (value === "none" || value === "short" || value === "long")
|
|
77
|
+
return value;
|
|
78
|
+
throw new Error(`Config value ${path} must be one of "none", "short", or "long"`);
|
|
79
|
+
}
|
|
80
|
+
function readThinkingLevel(value) {
|
|
81
|
+
if (value === "off" ||
|
|
82
|
+
value === "minimal" ||
|
|
83
|
+
value === "low" ||
|
|
84
|
+
value === "medium" ||
|
|
85
|
+
value === "high" ||
|
|
86
|
+
value === "xhigh") {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
throw new Error('Config value agent.thinking_level must be one of "off", "minimal", "low", "medium", "high", or "xhigh"');
|
|
90
|
+
}
|
|
91
|
+
function readDiscordReplyMode(value) {
|
|
92
|
+
if (value === "plain" || value === "reply")
|
|
93
|
+
return value;
|
|
94
|
+
throw new Error('Config value discord.reply_mode must be one of "plain" or "reply"');
|
|
95
|
+
}
|
|
96
|
+
function readDiscordChunkMode(value) {
|
|
97
|
+
if (value === "simple" || value === "paragraph" || value === "newline")
|
|
98
|
+
return value;
|
|
99
|
+
throw new Error('Config value discord.chunk_mode must be one of "simple", "paragraph", or "newline"');
|
|
100
|
+
}
|
|
101
|
+
function readDiscordDispatchMode(value, path) {
|
|
102
|
+
if (value === "steer" || value === "queue" || value === "collect")
|
|
103
|
+
return value;
|
|
104
|
+
throw new Error(`Config value ${path} must be one of "steer", "queue", or "collect"`);
|
|
105
|
+
}
|
|
106
|
+
function readDiscordChannelTrigger(value) {
|
|
107
|
+
if (value === "mention" || value === "always")
|
|
108
|
+
return value;
|
|
109
|
+
throw new Error('Config value discord.channel_trigger must be one of "mention" or "always"');
|
|
110
|
+
}
|
|
111
|
+
function readCronFrequency(value, path) {
|
|
112
|
+
if (value === "once" || value === "hourly" || value === "daily" || value === "weekly" || value === "monthly") {
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`Config value ${path} must be one of "once", "hourly", "daily", "weekly", or "monthly"`);
|
|
116
|
+
}
|
|
117
|
+
function readCronDeliveryMode(value, path) {
|
|
118
|
+
if (value === "queue" || value === "follow_up")
|
|
119
|
+
return value;
|
|
120
|
+
throw new Error(`Config value ${path} must be one of "queue" or "follow_up"`);
|
|
121
|
+
}
|
|
122
|
+
function readWebAuthMode(value) {
|
|
123
|
+
if (value === "tailscale-only" || value === "bearer" || value === "public-2fa")
|
|
124
|
+
return value;
|
|
125
|
+
throw new Error('Config value web.auth_mode must be one of "tailscale-only", "bearer", or "public-2fa"');
|
|
126
|
+
}
|
|
127
|
+
function readTtsProvider(value) {
|
|
128
|
+
if (value === "elevenlabs")
|
|
129
|
+
return value;
|
|
130
|
+
throw new Error('Config value tts.provider must be "elevenlabs"');
|
|
131
|
+
}
|
|
132
|
+
function readImageGenApi(value) {
|
|
133
|
+
if (value === "openrouter-images")
|
|
134
|
+
return value;
|
|
135
|
+
throw new Error('Config value image_gen.api must be "openrouter-images"');
|
|
136
|
+
}
|
|
137
|
+
function readMediaUnderstandingProvider(value) {
|
|
138
|
+
if (value === "groq" || value === "google")
|
|
139
|
+
return value;
|
|
140
|
+
throw new Error('Config value media_understanding provider must be "groq" or "google"');
|
|
141
|
+
}
|
|
142
|
+
function readMemoryEmbeddingFormat(value, path = "memory.embedding.format") {
|
|
143
|
+
if (value === "gemini" || value === "openai" || value === "voyage")
|
|
144
|
+
return value;
|
|
145
|
+
throw new Error(`Config value ${path} must be one of "gemini", "openai", or "voyage"`);
|
|
146
|
+
}
|
|
147
|
+
function readBrowserBackend(value) {
|
|
148
|
+
if (value === "opencli" || value === "browser-harness")
|
|
149
|
+
return value;
|
|
150
|
+
throw new Error('Config value browser.backend must be "opencli" or "browser-harness"');
|
|
151
|
+
}
|
|
152
|
+
function readBrowserWindowMode(value) {
|
|
153
|
+
if (value === "foreground" || value === "background")
|
|
154
|
+
return value;
|
|
155
|
+
throw new Error('Config value browser.window must be one of "foreground" or "background"');
|
|
156
|
+
}
|
|
157
|
+
function readBoolean(value, fallback, path) {
|
|
158
|
+
if (value === undefined)
|
|
159
|
+
return fallback;
|
|
160
|
+
if (typeof value === "boolean")
|
|
161
|
+
return value;
|
|
162
|
+
throw new Error(`Config value ${path} must be a boolean`);
|
|
163
|
+
}
|
|
164
|
+
function readInteger(value, fallback, path, min = 0) {
|
|
165
|
+
if (value === undefined)
|
|
166
|
+
return fallback;
|
|
167
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min) {
|
|
168
|
+
throw new Error(`Config value ${path} must be an integer >= ${min}`);
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
function resolveProviderSetting(records, provider, model) {
|
|
173
|
+
return records[`${provider}/${model}`] ?? records[provider];
|
|
174
|
+
}
|
|
175
|
+
function readNumberInRange(value, fallback, path, min, max) {
|
|
176
|
+
if (value === undefined)
|
|
177
|
+
return fallback;
|
|
178
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
|
|
179
|
+
throw new Error(`Config value ${path} must be a number between ${min} and ${max}`);
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
function readFraction(value, fallback, path) {
|
|
184
|
+
if (value === undefined)
|
|
185
|
+
return fallback;
|
|
186
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1) {
|
|
187
|
+
throw new Error(`Config value ${path} must be a number > 0 and <= 1`);
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
function readPositiveNumber(value, fallback, path) {
|
|
192
|
+
if (value === undefined)
|
|
193
|
+
return fallback;
|
|
194
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
195
|
+
throw new Error(`Config value ${path} must be a positive number`);
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
function readOptionalInteger(value, path, min = 0) {
|
|
200
|
+
if (value === undefined)
|
|
201
|
+
return undefined;
|
|
202
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min) {
|
|
203
|
+
throw new Error(`Config value ${path} must be an integer >= ${min}`);
|
|
204
|
+
}
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
function readIntegerInRange(value, fallback, path, min, max) {
|
|
208
|
+
const read = readInteger(value, fallback, path, min);
|
|
209
|
+
if (read > max)
|
|
210
|
+
throw new Error(`Config value ${path} must be an integer <= ${max}`);
|
|
211
|
+
return read;
|
|
212
|
+
}
|
|
213
|
+
function readOptionalIntegerInRange(value, path, min, max) {
|
|
214
|
+
const read = readOptionalInteger(value, path, min);
|
|
215
|
+
if (read !== undefined && read > max)
|
|
216
|
+
throw new Error(`Config value ${path} must be an integer <= ${max}`);
|
|
217
|
+
return read;
|
|
218
|
+
}
|
|
219
|
+
function assertCronTime(value, path) {
|
|
220
|
+
if (value === undefined)
|
|
221
|
+
return;
|
|
222
|
+
if (!/^([01]?\d|2[0-3]):([0-5]\d)$/.test(value)) {
|
|
223
|
+
throw new Error(`Config value ${path} must be HH:MM local time`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function assertCronRunAt(value, path) {
|
|
227
|
+
if (value === undefined)
|
|
228
|
+
return;
|
|
229
|
+
if (Number.isFinite(Date.parse(value)))
|
|
230
|
+
return;
|
|
231
|
+
if (/^\d{4}-\d{2}-\d{2}[ T]([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/.test(value))
|
|
232
|
+
return;
|
|
233
|
+
throw new Error(`Config value ${path} must be an ISO timestamp or YYYY-MM-DD HH:MM local time`);
|
|
234
|
+
}
|
|
235
|
+
function resolveWorkspacePath(workspacePath, filePath) {
|
|
236
|
+
return isAbsolute(filePath) ? filePath : resolve(workspacePath, filePath);
|
|
237
|
+
}
|
|
238
|
+
function parseProviderModelRef(value, path) {
|
|
239
|
+
const parsed = maybeParseProviderModelRef(value);
|
|
240
|
+
if (parsed)
|
|
241
|
+
return parsed;
|
|
242
|
+
throw new Error(`Config value ${path} must be a provider/model id`);
|
|
243
|
+
}
|
|
244
|
+
function maybeParseProviderModelRef(value) {
|
|
245
|
+
const trimmed = value.trim();
|
|
246
|
+
const separator = trimmed.indexOf("/");
|
|
247
|
+
if (separator <= 0 || separator === trimmed.length - 1)
|
|
248
|
+
return undefined;
|
|
249
|
+
const provider = trimmed.slice(0, separator).trim();
|
|
250
|
+
const modelId = trimmed.slice(separator + 1).trim();
|
|
251
|
+
if (!provider || !modelId)
|
|
252
|
+
return undefined;
|
|
253
|
+
return { provider, modelId, key: `${provider}/${modelId}` };
|
|
254
|
+
}
|
|
255
|
+
function assertKnownKeys(value, path, knownKeys) {
|
|
256
|
+
const known = new Set(knownKeys);
|
|
257
|
+
for (const key of Object.keys(value)) {
|
|
258
|
+
if (!known.has(key))
|
|
259
|
+
throw new Error(`Unknown config value: ${path}.${key}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function readPromptOverrides(value, workspacePath, prefix) {
|
|
263
|
+
const prompt = readOptionalConfigString(value.prompt, `${prefix}.prompt`);
|
|
264
|
+
const promptPath = readOptionalConfigString(value.prompt_path, `${prefix}.prompt_path`);
|
|
265
|
+
const systemPrompt = readOptionalConfigString(value.system_prompt, `${prefix}.system_prompt`);
|
|
266
|
+
const systemPromptPath = readOptionalConfigString(value.system_prompt_path, `${prefix}.system_prompt_path`);
|
|
267
|
+
if (prompt && promptPath)
|
|
268
|
+
throw new Error(`Set either ${prefix}.prompt or ${prefix}.prompt_path, not both`);
|
|
269
|
+
if (systemPrompt && systemPromptPath) {
|
|
270
|
+
throw new Error(`Set either ${prefix}.system_prompt or ${prefix}.system_prompt_path, not both`);
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
...(prompt ? { prompt } : {}),
|
|
274
|
+
...(promptPath ? { promptPath: resolveWorkspacePath(workspacePath, promptPath) } : {}),
|
|
275
|
+
...(systemPrompt ? { systemPrompt } : {}),
|
|
276
|
+
...(systemPromptPath ? { systemPromptPath: resolveWorkspacePath(workspacePath, systemPromptPath) } : {}),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function readCronJobs(cron) {
|
|
280
|
+
const rawJobs = cron.jobs;
|
|
281
|
+
if (rawJobs === undefined)
|
|
282
|
+
return [];
|
|
283
|
+
if (!Array.isArray(rawJobs))
|
|
284
|
+
throw new Error("Config value cron.jobs must be an array");
|
|
285
|
+
const seen = new Set();
|
|
286
|
+
return rawJobs.map((rawJob, index) => {
|
|
287
|
+
if (!rawJob || typeof rawJob !== "object" || Array.isArray(rawJob)) {
|
|
288
|
+
throw new Error(`Config value cron.jobs[${index}] must be a table`);
|
|
289
|
+
}
|
|
290
|
+
const job = rawJob;
|
|
291
|
+
const prefix = `cron.jobs[${index}]`;
|
|
292
|
+
assertKnownKeys(job, prefix, [
|
|
293
|
+
"id",
|
|
294
|
+
"enabled",
|
|
295
|
+
"frequency",
|
|
296
|
+
"delivery_mode",
|
|
297
|
+
"prompt",
|
|
298
|
+
"run_at",
|
|
299
|
+
"time",
|
|
300
|
+
"minute",
|
|
301
|
+
"weekday",
|
|
302
|
+
"day",
|
|
303
|
+
]);
|
|
304
|
+
const id = readString(job.id, `${prefix}.id`);
|
|
305
|
+
if (!/^[A-Za-z0-9._=-]+$/.test(id)) {
|
|
306
|
+
throw new Error(`Config value ${prefix}.id may only contain letters, numbers, dot, underscore, equals, or dash`);
|
|
307
|
+
}
|
|
308
|
+
if (seen.has(id))
|
|
309
|
+
throw new Error(`Duplicate cron job id: ${id}`);
|
|
310
|
+
seen.add(id);
|
|
311
|
+
const frequency = readCronFrequency(readConfigString(job.frequency, "once", `${prefix}.frequency`), `${prefix}.frequency`);
|
|
312
|
+
const runAt = readOptionalConfigString(job.run_at, `${prefix}.run_at`);
|
|
313
|
+
const time = readOptionalConfigString(job.time, `${prefix}.time`);
|
|
314
|
+
assertCronRunAt(runAt, `${prefix}.run_at`);
|
|
315
|
+
assertCronTime(time, `${prefix}.time`);
|
|
316
|
+
if (frequency === "once" && !runAt)
|
|
317
|
+
throw new Error(`Config value ${prefix}.run_at is required for once jobs`);
|
|
318
|
+
if (frequency === "once" && time)
|
|
319
|
+
throw new Error(`Config value ${prefix}.time is only valid for repeating jobs`);
|
|
320
|
+
if (frequency !== "once" && runAt)
|
|
321
|
+
throw new Error(`Config value ${prefix}.run_at is only valid for once jobs`);
|
|
322
|
+
if (frequency !== "once" && frequency !== "hourly" && !time) {
|
|
323
|
+
throw new Error(`Config value ${prefix}.time is required for ${frequency} jobs`);
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
id,
|
|
327
|
+
enabled: readBoolean(job.enabled, true, `${prefix}.enabled`),
|
|
328
|
+
frequency,
|
|
329
|
+
deliveryMode: readCronDeliveryMode(readConfigString(job.delivery_mode, "queue", `${prefix}.delivery_mode`), `${prefix}.delivery_mode`),
|
|
330
|
+
prompt: readString(job.prompt, `${prefix}.prompt`),
|
|
331
|
+
...(runAt ? { runAt } : {}),
|
|
332
|
+
...(time ? { time } : {}),
|
|
333
|
+
...(job.minute !== undefined
|
|
334
|
+
? { minute: readOptionalIntegerInRange(job.minute, `${prefix}.minute`, 0, 59) }
|
|
335
|
+
: {}),
|
|
336
|
+
...(job.weekday !== undefined
|
|
337
|
+
? { weekday: readOptionalIntegerInRange(job.weekday, `${prefix}.weekday`, 0, 6) }
|
|
338
|
+
: {}),
|
|
339
|
+
...(job.day !== undefined ? { day: readOptionalIntegerInRange(job.day, `${prefix}.day`, 1, 31) } : {}),
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
function readBrowserAllowedSites(browser) {
|
|
344
|
+
const rawSites = browser.sites;
|
|
345
|
+
if (rawSites === undefined)
|
|
346
|
+
return defaultBrowserAllowedSites();
|
|
347
|
+
if (!rawSites || typeof rawSites !== "object" || Array.isArray(rawSites)) {
|
|
348
|
+
throw new Error("Config value browser.sites must be a table");
|
|
349
|
+
}
|
|
350
|
+
const sites = {};
|
|
351
|
+
for (const [siteName, rawSite] of Object.entries(rawSites)) {
|
|
352
|
+
if (!rawSite || typeof rawSite !== "object" || Array.isArray(rawSite)) {
|
|
353
|
+
throw new Error(`Config value browser.sites.${siteName} must be a table`);
|
|
354
|
+
}
|
|
355
|
+
const site = rawSite;
|
|
356
|
+
assertKnownKeys(site, `browser.sites.${siteName}`, ["read", "write"]);
|
|
357
|
+
const read = readStringArray(site.read, `browser.sites.${siteName}.read`);
|
|
358
|
+
const write = readStringArray(site.write, `browser.sites.${siteName}.write`);
|
|
359
|
+
sites[siteName] = { read, write };
|
|
360
|
+
}
|
|
361
|
+
return sites;
|
|
362
|
+
}
|
|
363
|
+
function defaultBrowserAllowedSites() {
|
|
364
|
+
return {
|
|
365
|
+
twitter: {
|
|
366
|
+
read: [
|
|
367
|
+
"article",
|
|
368
|
+
"bookmark-folders",
|
|
369
|
+
"bookmarks",
|
|
370
|
+
"following",
|
|
371
|
+
"likes",
|
|
372
|
+
"list-tweets",
|
|
373
|
+
"lists",
|
|
374
|
+
"notifications",
|
|
375
|
+
"profile",
|
|
376
|
+
"search",
|
|
377
|
+
"thread",
|
|
378
|
+
"timeline",
|
|
379
|
+
"trending",
|
|
380
|
+
"tweets",
|
|
381
|
+
],
|
|
382
|
+
write: [],
|
|
383
|
+
},
|
|
384
|
+
xiaohongshu: {
|
|
385
|
+
read: [
|
|
386
|
+
"comments",
|
|
387
|
+
"creator-note-detail",
|
|
388
|
+
"creator-notes",
|
|
389
|
+
"creator-notes-summary",
|
|
390
|
+
"creator-profile",
|
|
391
|
+
"creator-stats",
|
|
392
|
+
"feed",
|
|
393
|
+
"note",
|
|
394
|
+
"notifications",
|
|
395
|
+
"search",
|
|
396
|
+
"user",
|
|
397
|
+
],
|
|
398
|
+
write: [],
|
|
399
|
+
},
|
|
400
|
+
rednote: {
|
|
401
|
+
read: ["comments", "feed", "note", "notifications", "search", "user"],
|
|
402
|
+
write: [],
|
|
403
|
+
},
|
|
404
|
+
reddit: {
|
|
405
|
+
read: [
|
|
406
|
+
"frontpage",
|
|
407
|
+
"home",
|
|
408
|
+
"hot",
|
|
409
|
+
"popular",
|
|
410
|
+
"read",
|
|
411
|
+
"saved",
|
|
412
|
+
"search",
|
|
413
|
+
"subreddit",
|
|
414
|
+
"subreddit-info",
|
|
415
|
+
"upvoted",
|
|
416
|
+
"user",
|
|
417
|
+
"user-comments",
|
|
418
|
+
"user-posts",
|
|
419
|
+
"whoami",
|
|
420
|
+
],
|
|
421
|
+
write: [],
|
|
422
|
+
},
|
|
423
|
+
bilibili: {
|
|
424
|
+
read: [
|
|
425
|
+
"comments",
|
|
426
|
+
"dynamic",
|
|
427
|
+
"feed",
|
|
428
|
+
"following",
|
|
429
|
+
"history",
|
|
430
|
+
"hot",
|
|
431
|
+
"me",
|
|
432
|
+
"ranking",
|
|
433
|
+
"search",
|
|
434
|
+
"subtitle",
|
|
435
|
+
"user-videos",
|
|
436
|
+
"video",
|
|
437
|
+
],
|
|
438
|
+
write: [],
|
|
439
|
+
},
|
|
440
|
+
youtube: {
|
|
441
|
+
read: [
|
|
442
|
+
"channel",
|
|
443
|
+
"comments",
|
|
444
|
+
"feed",
|
|
445
|
+
"history",
|
|
446
|
+
"playlist",
|
|
447
|
+
"search",
|
|
448
|
+
"subscriptions",
|
|
449
|
+
"transcript",
|
|
450
|
+
"video",
|
|
451
|
+
"watch-later",
|
|
452
|
+
],
|
|
453
|
+
write: [],
|
|
454
|
+
},
|
|
455
|
+
tiktok: {
|
|
456
|
+
read: ["explore", "friends", "live", "notifications", "profile", "search", "user"],
|
|
457
|
+
write: [],
|
|
458
|
+
},
|
|
459
|
+
douyin: {
|
|
460
|
+
read: [
|
|
461
|
+
"activities",
|
|
462
|
+
"collections",
|
|
463
|
+
"drafts",
|
|
464
|
+
"hashtag",
|
|
465
|
+
"location",
|
|
466
|
+
"profile",
|
|
467
|
+
"stats",
|
|
468
|
+
"user-videos",
|
|
469
|
+
"videos",
|
|
470
|
+
],
|
|
471
|
+
write: [],
|
|
472
|
+
},
|
|
473
|
+
spotify: {
|
|
474
|
+
read: ["search", "status"],
|
|
475
|
+
write: [],
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
export async function loadConfig(workspacePathInput) {
|
|
480
|
+
const workspacePath = resolve(workspacePathInput);
|
|
481
|
+
const configPath = resolve(workspacePath, "config.toml");
|
|
482
|
+
if (!existsSync(configPath)) {
|
|
483
|
+
throw new Error(`Missing config.toml in workspace: ${workspacePath}`);
|
|
484
|
+
}
|
|
485
|
+
const raw = await readFile(configPath, "utf8");
|
|
486
|
+
const parsed = interpolateValue(parse(raw));
|
|
487
|
+
const discord = (parsed.discord ?? {});
|
|
488
|
+
const web = (parsed.web ?? {});
|
|
489
|
+
const browser = (parsed.browser ?? {});
|
|
490
|
+
const agent = (parsed.agent ?? {});
|
|
491
|
+
const heartbeat = (parsed.heartbeat ?? {});
|
|
492
|
+
const cron = (parsed.cron ?? {});
|
|
493
|
+
const models = (parsed.models ?? {});
|
|
494
|
+
const tts = (parsed.tts ?? {});
|
|
495
|
+
const ttsVoiceSettings = (tts.voice_settings ?? {});
|
|
496
|
+
const imageGen = (parsed.image_gen ?? {});
|
|
497
|
+
const media = (parsed.media ?? {});
|
|
498
|
+
const generatedMedia = (media.generated ?? {});
|
|
499
|
+
const data = (parsed.data ?? {});
|
|
500
|
+
const dataChat = (data.chat ?? {});
|
|
501
|
+
const dataTranscripts = (data.transcripts ?? {});
|
|
502
|
+
const dataPayloads = (data.payloads ?? {});
|
|
503
|
+
const mediaUnderstanding = (media.understanding ?? {});
|
|
504
|
+
const mediaUnderstandingAudio = (mediaUnderstanding.audio ?? {});
|
|
505
|
+
const mediaUnderstandingVideo = (mediaUnderstanding.video ?? {});
|
|
506
|
+
const persona = (parsed.persona ?? {});
|
|
507
|
+
const workspace = (parsed.workspace ?? {});
|
|
508
|
+
const memory = (parsed.memory ?? {});
|
|
509
|
+
const memoryEmbedding = (memory.embedding ?? {});
|
|
510
|
+
const memoryAmbient = (memory.ambient ?? {});
|
|
511
|
+
const memoryLcm = (memory.lcm ?? {});
|
|
512
|
+
const ownerId = readString(discord.owner_id, "discord.owner_id");
|
|
513
|
+
const model = readOptionalString(agent.model, "");
|
|
514
|
+
const provider = readOptionalString(agent.provider, "custom");
|
|
515
|
+
const modelId = readOptionalString(agent.model_id, "");
|
|
516
|
+
const api = readOptionalString(agent.api, "");
|
|
517
|
+
const baseUrl = readOptionalString(agent.base_url, "");
|
|
518
|
+
const apiKeyEnv = readOptionalString(agent.api_key_env, "");
|
|
519
|
+
const legacyModel = modelId ? `${provider}/${modelId}` : "";
|
|
520
|
+
const agentModel = model || legacyModel;
|
|
521
|
+
const usingLegacyAgentModel = !model;
|
|
522
|
+
let agentCacheRetentionRaw = agent.cache_retention;
|
|
523
|
+
if (agentCacheRetentionRaw === undefined && agent.cacheRetention !== undefined) {
|
|
524
|
+
warnOnce("agent.cacheRetention", "Config value agent.cacheRetention is deprecated; use agent.cache_retention instead.");
|
|
525
|
+
agentCacheRetentionRaw = agent.cacheRetention;
|
|
526
|
+
}
|
|
527
|
+
if (usingLegacyAgentModel && (!api || !modelId || !baseUrl || !apiKeyEnv)) {
|
|
528
|
+
throw new Error('Set agent.model = "provider/model", or for a legacy custom endpoint set all of agent.api, agent.model_id, agent.base_url, and agent.api_key_env.');
|
|
529
|
+
}
|
|
530
|
+
const memoryRootDir = resolveWorkspacePath(workspacePath, readOptionalString(memory.root_dir, "memories"));
|
|
531
|
+
const modelAllow = readStringArray(models.allow, "models.allow");
|
|
532
|
+
const modelBaseUrls = readStringRecord(models.base_urls, "models.base_urls");
|
|
533
|
+
const modelApiKeyEnvs = readStringRecord(models.api_key_envs, "models.api_key_envs");
|
|
534
|
+
let memoryEmbeddingFormatRaw = memoryEmbedding.format;
|
|
535
|
+
if (memoryEmbeddingFormatRaw === undefined && memoryEmbedding.api !== undefined) {
|
|
536
|
+
warnOnce("memory.embedding.api", "Config value memory.embedding.api is deprecated; use memory.embedding.format instead.");
|
|
537
|
+
memoryEmbeddingFormatRaw = memoryEmbedding.api;
|
|
538
|
+
}
|
|
539
|
+
const memoryEmbeddingFormat = readMemoryEmbeddingFormat(readOptionalString(memoryEmbeddingFormatRaw, "gemini"), memoryEmbedding.format === undefined && memoryEmbedding.api !== undefined
|
|
540
|
+
? "memory.embedding.api"
|
|
541
|
+
: "memory.embedding.format");
|
|
542
|
+
const memoryEmbeddingProvider = readConfigString(memoryEmbedding.provider, "google", "memory.embedding.provider");
|
|
543
|
+
const memoryEmbeddingModel = readConfigString(memoryEmbedding.model, "gemini-embedding-2", "memory.embedding.model");
|
|
544
|
+
const memoryEmbeddingBaseUrl = readOptionalConfigString(memoryEmbedding.base_url, "memory.embedding.base_url") ??
|
|
545
|
+
resolveProviderSetting(modelBaseUrls, memoryEmbeddingProvider, memoryEmbeddingModel) ??
|
|
546
|
+
DEFAULT_MEMORY_EMBEDDING_BASE_URLS[memoryEmbeddingProvider] ??
|
|
547
|
+
"";
|
|
548
|
+
const memoryEmbeddingApiKeyEnv = readOptionalConfigString(memoryEmbedding.api_key_env, "memory.embedding.api_key_env") ??
|
|
549
|
+
resolveProviderSetting(modelApiKeyEnvs, memoryEmbeddingProvider, memoryEmbeddingModel) ??
|
|
550
|
+
DEFAULT_MEMORY_EMBEDDING_API_KEY_ENVS[memoryEmbeddingProvider] ??
|
|
551
|
+
"";
|
|
552
|
+
if (!memoryEmbeddingBaseUrl) {
|
|
553
|
+
throw new Error(`Missing memory.embedding.base_url for provider: ${memoryEmbeddingProvider}`);
|
|
554
|
+
}
|
|
555
|
+
// Empty apiKeyEnv is allowed for no-auth local gateways. Hosted providers
|
|
556
|
+
// should either inherit from models.api_key_envs or set memory.embedding.api_key_env.
|
|
557
|
+
assertKnownKeys(memoryLcm, "memory.lcm", [
|
|
558
|
+
"enabled",
|
|
559
|
+
"model",
|
|
560
|
+
"context_threshold",
|
|
561
|
+
"fresh_tail_count",
|
|
562
|
+
"fresh_tail_max_tokens",
|
|
563
|
+
"leaf_chunk_tokens",
|
|
564
|
+
"leaf_target_tokens",
|
|
565
|
+
"prompt_aware_eviction_enabled",
|
|
566
|
+
"condense_group_size",
|
|
567
|
+
"max_summary_depth",
|
|
568
|
+
"new_session_retain_depth",
|
|
569
|
+
"max_rounds",
|
|
570
|
+
"cache_ttl_ms",
|
|
571
|
+
"cache_touch_slack_ms",
|
|
572
|
+
"critical_overflow_tokens",
|
|
573
|
+
"timeout_ms",
|
|
574
|
+
"prompt",
|
|
575
|
+
"prompt_path",
|
|
576
|
+
"system_prompt",
|
|
577
|
+
"system_prompt_path",
|
|
578
|
+
]);
|
|
579
|
+
const memoryLcmEnabled = readBoolean(memoryLcm.enabled, true, "memory.lcm.enabled");
|
|
580
|
+
let memoryLcmRef;
|
|
581
|
+
let memoryLcmBaseUrl;
|
|
582
|
+
let memoryLcmApiKeyEnv;
|
|
583
|
+
let memoryLcmFreshTailMaxTokens;
|
|
584
|
+
let memoryLcmLeafChunkTokens = 20_000;
|
|
585
|
+
let memoryLcmLeafTargetTokens = 2400;
|
|
586
|
+
let memoryLcmPromptOverrides = {};
|
|
587
|
+
if (memoryLcmEnabled) {
|
|
588
|
+
const memoryLcmModel = readConfigString(memoryLcm.model, agentModel, "memory.lcm.model");
|
|
589
|
+
memoryLcmRef = parseProviderModelRef(memoryLcmModel, "memory.lcm.model");
|
|
590
|
+
if (modelAllow.length > 0 && !modelAllow.includes(memoryLcmRef.key)) {
|
|
591
|
+
throw new Error(`Config value memory.lcm.model is not in models.allow: ${memoryLcmRef.key}`);
|
|
592
|
+
}
|
|
593
|
+
memoryLcmBaseUrl =
|
|
594
|
+
resolveProviderSetting(modelBaseUrls, memoryLcmRef.provider, memoryLcmRef.modelId) ??
|
|
595
|
+
(usingLegacyAgentModel && memoryLcmRef.key === legacyModel ? baseUrl : undefined);
|
|
596
|
+
memoryLcmApiKeyEnv =
|
|
597
|
+
resolveProviderSetting(modelApiKeyEnvs, memoryLcmRef.provider, memoryLcmRef.modelId) ??
|
|
598
|
+
(usingLegacyAgentModel && memoryLcmRef.provider === provider ? apiKeyEnv : undefined);
|
|
599
|
+
memoryLcmFreshTailMaxTokens = readOptionalInteger(memoryLcm.fresh_tail_max_tokens, "memory.lcm.fresh_tail_max_tokens", 1);
|
|
600
|
+
memoryLcmLeafChunkTokens = readInteger(memoryLcm.leaf_chunk_tokens, 20_000, "memory.lcm.leaf_chunk_tokens", 1);
|
|
601
|
+
memoryLcmLeafTargetTokens = readInteger(memoryLcm.leaf_target_tokens, 2400, "memory.lcm.leaf_target_tokens", 1);
|
|
602
|
+
if (memoryLcmLeafTargetTokens > memoryLcmLeafChunkTokens) {
|
|
603
|
+
throw new Error("Config value memory.lcm.leaf_target_tokens must be <= leaf_chunk_tokens");
|
|
604
|
+
}
|
|
605
|
+
if (memoryLcmFreshTailMaxTokens !== undefined && memoryLcmFreshTailMaxTokens > memoryLcmLeafChunkTokens) {
|
|
606
|
+
throw new Error("Config value memory.lcm.fresh_tail_max_tokens must be <= leaf_chunk_tokens");
|
|
607
|
+
}
|
|
608
|
+
memoryLcmPromptOverrides = readPromptOverrides(memoryLcm, workspacePath, "memory.lcm");
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
// Disabled LCM configs may keep stale summarizer-only fields; runtime checks enabled before using them.
|
|
612
|
+
const rawModel = typeof memoryLcm.model === "string" && memoryLcm.model.trim() ? memoryLcm.model : agentModel;
|
|
613
|
+
memoryLcmRef = maybeParseProviderModelRef(rawModel) ?? { provider: "", modelId: "", key: rawModel };
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
workspacePath,
|
|
617
|
+
discord: {
|
|
618
|
+
token: readString(process.env.DISCORD_TOKEN, "DISCORD_TOKEN"),
|
|
619
|
+
ownerId,
|
|
620
|
+
allowedChannels: readStringArray(discord.allowed_channels, "discord.allowed_channels"),
|
|
621
|
+
replyMode: readDiscordReplyMode(readOptionalString(discord.reply_mode, "plain")),
|
|
622
|
+
chunkMode: readDiscordChunkMode(readOptionalString(discord.chunk_mode, "paragraph")),
|
|
623
|
+
dmMode: readDiscordDispatchMode(readOptionalString(discord.dm_mode, "steer"), "discord.dm_mode"),
|
|
624
|
+
channelMode: readDiscordDispatchMode(readOptionalString(discord.channel_mode, "collect"), "discord.channel_mode"),
|
|
625
|
+
channelTrigger: readDiscordChannelTrigger(readOptionalString(discord.channel_trigger, "mention")),
|
|
626
|
+
collectDebounceMs: readInteger(discord.collect_debounce_ms, 4000, "discord.collect_debounce_ms"),
|
|
627
|
+
allowBotMessages: readBoolean(discord.allow_bot_messages, false, "discord.allow_bot_messages"),
|
|
628
|
+
},
|
|
629
|
+
web: {
|
|
630
|
+
port: readInteger(web.port, 8787, "web.port"),
|
|
631
|
+
authMode: readWebAuthMode(readOptionalString(web.auth_mode, "tailscale-only")),
|
|
632
|
+
bearerToken: readOptionalString(web.bearer_token, "") || undefined,
|
|
633
|
+
totpSecret: readOptionalString(web.totp_secret, "") || undefined,
|
|
634
|
+
bindAddress: readOptionalString(web.bind_address, "127.0.0.1"),
|
|
635
|
+
},
|
|
636
|
+
browser: {
|
|
637
|
+
enabled: readBoolean(browser.enabled, false, "browser.enabled"),
|
|
638
|
+
backend: readBrowserBackend(readOptionalString(browser.backend, "opencli")),
|
|
639
|
+
opencliCommand: readOptionalString(browser.opencli_command, "opencli"),
|
|
640
|
+
harnessCommand: readOptionalString(browser.harness_command, "browser-harness"),
|
|
641
|
+
session: readOptionalString(browser.session, "familiar"),
|
|
642
|
+
profile: readOptionalString(browser.profile, "") || undefined,
|
|
643
|
+
windowMode: readBrowserWindowMode(readOptionalString(browser.window, "background")),
|
|
644
|
+
timeoutMs: readInteger(browser.timeout_ms, 60_000, "browser.timeout_ms", 1),
|
|
645
|
+
maxOutputChars: readInteger(browser.max_output_chars, 12_000, "browser.max_output_chars", 1000),
|
|
646
|
+
readWrite: readBoolean(browser.read_write, false, "browser.read_write"),
|
|
647
|
+
allowedSites: readBrowserAllowedSites(browser),
|
|
648
|
+
},
|
|
649
|
+
agent: {
|
|
650
|
+
model: agentModel,
|
|
651
|
+
api: usingLegacyAgentModel ? api : undefined,
|
|
652
|
+
modelId: usingLegacyAgentModel ? modelId : undefined,
|
|
653
|
+
baseUrl: usingLegacyAgentModel ? baseUrl : undefined,
|
|
654
|
+
apiKeyEnv: usingLegacyAgentModel ? apiKeyEnv : undefined,
|
|
655
|
+
provider: usingLegacyAgentModel ? provider : undefined,
|
|
656
|
+
cacheRetention: readCacheRetention(readOptionalString(agentCacheRetentionRaw, "long"), agent.cache_retention === undefined && agent.cacheRetention !== undefined
|
|
657
|
+
? "agent.cacheRetention"
|
|
658
|
+
: "agent.cache_retention"),
|
|
659
|
+
thinkingLevel: readThinkingLevel(readOptionalString(agent.thinking_level, "medium")),
|
|
660
|
+
},
|
|
661
|
+
heartbeat: {
|
|
662
|
+
enabled: readBoolean(heartbeat.enabled, false, "heartbeat.enabled"),
|
|
663
|
+
idleThresholdMs: readInteger(heartbeat.idle_threshold_minutes, 60, "heartbeat.idle_threshold_minutes", 1) * 60_000,
|
|
664
|
+
intervalMs: readInteger(heartbeat.interval_minutes, 240, "heartbeat.interval_minutes", 1) * 60_000,
|
|
665
|
+
},
|
|
666
|
+
cron: {
|
|
667
|
+
enabled: readBoolean(cron.enabled, false, "cron.enabled"),
|
|
668
|
+
pollMs: readIntegerInRange(cron.poll_seconds, 60, "cron.poll_seconds", 1, 3600) * 1000,
|
|
669
|
+
jobs: readCronJobs(cron),
|
|
670
|
+
},
|
|
671
|
+
models: {
|
|
672
|
+
allow: modelAllow,
|
|
673
|
+
baseUrls: modelBaseUrls,
|
|
674
|
+
apiKeyEnvs: modelApiKeyEnvs,
|
|
675
|
+
},
|
|
676
|
+
tts: {
|
|
677
|
+
provider: readTtsProvider(readOptionalString(tts.provider, "elevenlabs")),
|
|
678
|
+
apiKeyEnv: readOptionalString(tts.api_key_env, "ELEVENLABS_API_KEY"),
|
|
679
|
+
voiceId: readOptionalString(tts.voice_id, ""),
|
|
680
|
+
modelId: readOptionalString(tts.model_id, "eleven_multilingual_v2"),
|
|
681
|
+
outputFormat: readOptionalString(tts.output_format, "mp3_44100_128"),
|
|
682
|
+
maxInputChars: readInteger(tts.max_input_chars, 5000, "tts.max_input_chars"),
|
|
683
|
+
voiceSettings: {
|
|
684
|
+
stability: readNumberInRange(ttsVoiceSettings.stability, 0.5, "tts.voice_settings.stability", 0, 1),
|
|
685
|
+
similarityBoost: readNumberInRange(ttsVoiceSettings.similarity_boost, 0.75, "tts.voice_settings.similarity_boost", 0, 1),
|
|
686
|
+
style: readNumberInRange(ttsVoiceSettings.style, 0, "tts.voice_settings.style", 0, 1),
|
|
687
|
+
speed: readPositiveNumber(ttsVoiceSettings.speed, 1, "tts.voice_settings.speed"),
|
|
688
|
+
useSpeakerBoost: readBoolean(ttsVoiceSettings.use_speaker_boost, true, "tts.voice_settings.use_speaker_boost"),
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
imageGen: {
|
|
692
|
+
enabled: readBoolean(imageGen.enabled, true, "image_gen.enabled"),
|
|
693
|
+
model: readConfigString(imageGen.model, "openrouter/google/gemini-2.5-flash-image", "image_gen.model"),
|
|
694
|
+
fallbackModel: readOptionalConfigString(imageGen.fallback_model, "image_gen.fallback_model"),
|
|
695
|
+
api: readImageGenApi(readOptionalString(imageGen.api, "openrouter-images")),
|
|
696
|
+
timeoutMs: readInteger(imageGen.timeout_ms, 120_000, "image_gen.timeout_ms", 1),
|
|
697
|
+
},
|
|
698
|
+
mediaUnderstanding: {
|
|
699
|
+
audio: {
|
|
700
|
+
provider: readMediaUnderstandingProvider(readOptionalString(mediaUnderstandingAudio.provider, "groq")),
|
|
701
|
+
model: readOptionalString(mediaUnderstandingAudio.model, "whisper-large-v3"),
|
|
702
|
+
apiKeyEnv: readOptionalString(mediaUnderstandingAudio.api_key_env, "GROQ_API_KEY"),
|
|
703
|
+
},
|
|
704
|
+
video: {
|
|
705
|
+
provider: readMediaUnderstandingProvider(readOptionalString(mediaUnderstandingVideo.provider, "google")),
|
|
706
|
+
model: readOptionalString(mediaUnderstandingVideo.model, "gemini-3-flash-preview"),
|
|
707
|
+
apiKeyEnv: readOptionalString(mediaUnderstandingVideo.api_key_env, "GEMINI_API_KEY"),
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
persona: {
|
|
711
|
+
soul: resolveWorkspacePath(workspacePath, readOptionalString(persona.soul, "SOUL.md")),
|
|
712
|
+
user: resolveWorkspacePath(workspacePath, readOptionalString(persona.user, "USER.md")),
|
|
713
|
+
memory: resolveWorkspacePath(workspacePath, readOptionalString(persona.memory, "MEMORY.md")),
|
|
714
|
+
inner: resolveWorkspacePath(workspacePath, readOptionalString(persona.inner, "INNER.md")),
|
|
715
|
+
},
|
|
716
|
+
media: {
|
|
717
|
+
generatedRetentionDays: readInteger(generatedMedia.retention_days, 30, "media.generated.retention_days"),
|
|
718
|
+
},
|
|
719
|
+
data: {
|
|
720
|
+
chat: {
|
|
721
|
+
retentionDays: readInteger(dataChat.retention_days, 0, "data.chat.retention_days"),
|
|
722
|
+
},
|
|
723
|
+
transcripts: {
|
|
724
|
+
retentionDays: readInteger(dataTranscripts.retention_days, 0, "data.transcripts.retention_days"),
|
|
725
|
+
},
|
|
726
|
+
payloads: {
|
|
727
|
+
retentionDays: readInteger(dataPayloads.retention_days, 7, "data.payloads.retention_days"),
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
workspace: {
|
|
731
|
+
dataDir: resolveWorkspacePath(workspacePath, readOptionalString(workspace.data_dir, "data")),
|
|
732
|
+
},
|
|
733
|
+
memory: {
|
|
734
|
+
rootDir: memoryRootDir,
|
|
735
|
+
indexDir: resolve(memoryRootDir, "index"),
|
|
736
|
+
lcmDir: resolve(memoryRootDir, "lcm"),
|
|
737
|
+
diariesDir: resolve(memoryRootDir, "diaries"),
|
|
738
|
+
archiveDir: resolve(memoryRootDir, "archive"),
|
|
739
|
+
embedding: {
|
|
740
|
+
format: memoryEmbeddingFormat,
|
|
741
|
+
api: memoryEmbeddingFormat,
|
|
742
|
+
provider: memoryEmbeddingProvider,
|
|
743
|
+
model: memoryEmbeddingModel,
|
|
744
|
+
baseUrl: memoryEmbeddingBaseUrl,
|
|
745
|
+
apiKeyEnv: memoryEmbeddingApiKeyEnv,
|
|
746
|
+
dimensions: readInteger(memoryEmbedding.dimensions, 3072, "memory.embedding.dimensions", 1),
|
|
747
|
+
batchSize: readInteger(memoryEmbedding.batch_size, 32, "memory.embedding.batch_size", 1),
|
|
748
|
+
},
|
|
749
|
+
ambient: {
|
|
750
|
+
enabled: readBoolean(memoryAmbient.enabled, true, "memory.ambient.enabled"),
|
|
751
|
+
topK: readInteger(memoryAmbient.top_k, 3, "memory.ambient.top_k", 1),
|
|
752
|
+
minQueryLength: readInteger(memoryAmbient.min_query_length, 8, "memory.ambient.min_query_length"),
|
|
753
|
+
throttleSeconds: readInteger(memoryAmbient.throttle_seconds, 30, "memory.ambient.throttle_seconds"),
|
|
754
|
+
weightSimilarity: readPositiveNumber(memoryAmbient.weight_similarity, 1.0, "memory.ambient.weight_similarity"),
|
|
755
|
+
weightValence: readNumberInRange(memoryAmbient.weight_valence, 0.08, "memory.ambient.weight_valence", 0, 10),
|
|
756
|
+
weightRecency: readNumberInRange(memoryAmbient.weight_recency, 0.08, "memory.ambient.weight_recency", 0, 10),
|
|
757
|
+
weightIntensity: readNumberInRange(memoryAmbient.weight_intensity, 0.1, "memory.ambient.weight_intensity", 0, 10),
|
|
758
|
+
},
|
|
759
|
+
lcm: {
|
|
760
|
+
enabled: memoryLcmEnabled,
|
|
761
|
+
model: memoryLcmRef.key,
|
|
762
|
+
provider: memoryLcmRef.provider,
|
|
763
|
+
modelId: memoryLcmRef.modelId,
|
|
764
|
+
...(memoryLcmBaseUrl !== undefined ? { baseUrl: memoryLcmBaseUrl } : {}),
|
|
765
|
+
...(memoryLcmApiKeyEnv !== undefined ? { apiKeyEnv: memoryLcmApiKeyEnv } : {}),
|
|
766
|
+
contextThreshold: memoryLcmEnabled
|
|
767
|
+
? readFraction(memoryLcm.context_threshold, 0.75, "memory.lcm.context_threshold")
|
|
768
|
+
: 0.75,
|
|
769
|
+
freshTailCount: memoryLcmEnabled
|
|
770
|
+
? readInteger(memoryLcm.fresh_tail_count, 64, "memory.lcm.fresh_tail_count")
|
|
771
|
+
: 64,
|
|
772
|
+
...(memoryLcmFreshTailMaxTokens !== undefined ? { freshTailMaxTokens: memoryLcmFreshTailMaxTokens } : {}),
|
|
773
|
+
leafChunkTokens: memoryLcmLeafChunkTokens,
|
|
774
|
+
leafTargetTokens: memoryLcmLeafTargetTokens,
|
|
775
|
+
promptAwareEvictionEnabled: memoryLcmEnabled
|
|
776
|
+
? readBoolean(memoryLcm.prompt_aware_eviction_enabled, true, "memory.lcm.prompt_aware_eviction_enabled")
|
|
777
|
+
: true,
|
|
778
|
+
condenseGroupSize: memoryLcmEnabled
|
|
779
|
+
? readInteger(memoryLcm.condense_group_size, 4, "memory.lcm.condense_group_size", 1)
|
|
780
|
+
: 4,
|
|
781
|
+
maxSummaryDepth: memoryLcmEnabled
|
|
782
|
+
? readInteger(memoryLcm.max_summary_depth, 2, "memory.lcm.max_summary_depth", 1)
|
|
783
|
+
: 2,
|
|
784
|
+
newSessionRetainDepth: memoryLcmEnabled
|
|
785
|
+
? readInteger(memoryLcm.new_session_retain_depth, 2, "memory.lcm.new_session_retain_depth", -1)
|
|
786
|
+
: 2,
|
|
787
|
+
maxRounds: memoryLcmEnabled ? readInteger(memoryLcm.max_rounds, 10, "memory.lcm.max_rounds", 1) : 10,
|
|
788
|
+
cacheTtlMs: memoryLcmEnabled
|
|
789
|
+
? readInteger(memoryLcm.cache_ttl_ms, 300_000, "memory.lcm.cache_ttl_ms", 1)
|
|
790
|
+
: 300_000,
|
|
791
|
+
cacheTouchSlackMs: memoryLcmEnabled
|
|
792
|
+
? readInteger(memoryLcm.cache_touch_slack_ms, 30_000, "memory.lcm.cache_touch_slack_ms", 0)
|
|
793
|
+
: 30_000,
|
|
794
|
+
criticalOverflowTokens: memoryLcmEnabled
|
|
795
|
+
? readInteger(memoryLcm.critical_overflow_tokens, 8000, "memory.lcm.critical_overflow_tokens", 1)
|
|
796
|
+
: 8000,
|
|
797
|
+
timeoutMs: memoryLcmEnabled
|
|
798
|
+
? readInteger(memoryLcm.timeout_ms, 60_000, "memory.lcm.timeout_ms", 1)
|
|
799
|
+
: 60_000,
|
|
800
|
+
...memoryLcmPromptOverrides,
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
}
|