@p-sw/brainbox 0.1.2-alpha.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { readFileSync as readFileSync2 } from "fs";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
|
-
import { dirname as
|
|
7
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/utils/logger.ts
|
|
10
10
|
import chalk from "chalk";
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
createWriteStream,
|
|
15
|
+
writeFileSync
|
|
16
|
+
} from "fs";
|
|
17
|
+
import { join } from "path";
|
|
13
18
|
var LEVELS = {
|
|
14
19
|
debug: { rank: 0, color: chalk.gray, stderr: false },
|
|
15
20
|
info: { rank: 1, color: chalk.blue, stderr: false },
|
|
@@ -26,29 +31,61 @@ var ICONS = {
|
|
|
26
31
|
error: "✖",
|
|
27
32
|
fatal: "▲"
|
|
28
33
|
};
|
|
34
|
+
function pad2(n) {
|
|
35
|
+
return n < 10 ? `0${n}` : `${n}`;
|
|
36
|
+
}
|
|
37
|
+
function dateKey(d = new Date) {
|
|
38
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class DailyFileSink {
|
|
42
|
+
dir;
|
|
43
|
+
date;
|
|
44
|
+
stream;
|
|
45
|
+
setDir(dir) {
|
|
46
|
+
if (dir === this.dir)
|
|
47
|
+
return;
|
|
48
|
+
this.stream?.end();
|
|
49
|
+
this.stream = undefined;
|
|
50
|
+
this.date = undefined;
|
|
51
|
+
this.dir = dir;
|
|
52
|
+
if (dir && !existsSync(dir))
|
|
53
|
+
mkdirSync(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
write(data) {
|
|
56
|
+
if (!this.dir)
|
|
57
|
+
return;
|
|
58
|
+
const today = dateKey();
|
|
59
|
+
if (!this.stream || this.date !== today) {
|
|
60
|
+
this.stream?.end();
|
|
61
|
+
this.date = today;
|
|
62
|
+
if (!existsSync(this.dir))
|
|
63
|
+
mkdirSync(this.dir, { recursive: true });
|
|
64
|
+
this.stream = createWriteStream(join(this.dir, `${today}.log`), {
|
|
65
|
+
flags: "a"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
this.stream.write(data);
|
|
69
|
+
}
|
|
70
|
+
close() {
|
|
71
|
+
this.stream?.end();
|
|
72
|
+
this.stream = undefined;
|
|
73
|
+
this.date = undefined;
|
|
74
|
+
this.dir = undefined;
|
|
75
|
+
}
|
|
76
|
+
get enabled() {
|
|
77
|
+
return this.dir !== undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
29
80
|
var shared = {
|
|
30
81
|
level: "info",
|
|
31
82
|
timestamps: true,
|
|
32
83
|
colors: chalk.level > 0,
|
|
33
|
-
file: undefined,
|
|
34
|
-
fileStream: undefined,
|
|
35
84
|
json: false,
|
|
36
85
|
silent: false
|
|
37
86
|
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return;
|
|
41
|
-
shared.fileStream?.end();
|
|
42
|
-
shared.file = path;
|
|
43
|
-
if (path) {
|
|
44
|
-
const dir = dirname(path);
|
|
45
|
-
if (!existsSync(dir))
|
|
46
|
-
mkdirSync(dir, { recursive: true });
|
|
47
|
-
shared.fileStream = createWriteStream(path, { flags: "a" });
|
|
48
|
-
} else {
|
|
49
|
-
shared.fileStream = undefined;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
87
|
+
var mainSink = new DailyFileSink;
|
|
88
|
+
var llmLogDir;
|
|
52
89
|
function applyShared(options) {
|
|
53
90
|
if (options.level !== undefined)
|
|
54
91
|
shared.level = options.level;
|
|
@@ -60,8 +97,8 @@ function applyShared(options) {
|
|
|
60
97
|
shared.silent = options.silent;
|
|
61
98
|
if (options.json !== undefined)
|
|
62
99
|
shared.json = options.json;
|
|
63
|
-
if ("
|
|
64
|
-
|
|
100
|
+
if ("logDir" in options)
|
|
101
|
+
mainSink.setDir(options.logDir);
|
|
65
102
|
}
|
|
66
103
|
|
|
67
104
|
class Logger {
|
|
@@ -75,8 +112,7 @@ class Logger {
|
|
|
75
112
|
}
|
|
76
113
|
formatTimestamp() {
|
|
77
114
|
const now = new Date;
|
|
78
|
-
|
|
79
|
-
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
115
|
+
return `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())} ${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`;
|
|
80
116
|
}
|
|
81
117
|
format(level, message) {
|
|
82
118
|
const ts = shared.timestamps ? `[${this.formatTimestamp()}]` : "";
|
|
@@ -114,8 +150,8 @@ class Logger {
|
|
|
114
150
|
out.write(consoleLine + `
|
|
115
151
|
`);
|
|
116
152
|
}
|
|
117
|
-
if (
|
|
118
|
-
|
|
153
|
+
if (mainSink.enabled) {
|
|
154
|
+
mainSink.write(shared.json ? jsonLine : fileLine);
|
|
119
155
|
}
|
|
120
156
|
}
|
|
121
157
|
debug(message) {
|
|
@@ -146,22 +182,48 @@ class Logger {
|
|
|
146
182
|
applyShared(options);
|
|
147
183
|
}
|
|
148
184
|
close() {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
shared.file = undefined;
|
|
185
|
+
mainSink.close();
|
|
186
|
+
llmLogDir = undefined;
|
|
152
187
|
}
|
|
153
188
|
}
|
|
154
189
|
var logger = new Logger;
|
|
190
|
+
function configureLlmLog(logDir) {
|
|
191
|
+
llmLogDir = logDir;
|
|
192
|
+
if (logDir && !existsSync(logDir))
|
|
193
|
+
mkdirSync(logDir, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
function isLlmLogEnabled() {
|
|
196
|
+
return llmLogDir !== undefined;
|
|
197
|
+
}
|
|
198
|
+
function sanitizeCaller(caller) {
|
|
199
|
+
const s = caller.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
200
|
+
return s.length > 0 ? s : "unknown";
|
|
201
|
+
}
|
|
202
|
+
function dateTimeKey(d = new Date) {
|
|
203
|
+
return `${dateKey(d)}-${pad2(d.getHours())}-${pad2(d.getMinutes())}-${pad2(d.getSeconds())}`;
|
|
204
|
+
}
|
|
205
|
+
function writeLlmExchange(caller, content) {
|
|
206
|
+
if (!llmLogDir)
|
|
207
|
+
return;
|
|
208
|
+
if (!existsSync(llmLogDir))
|
|
209
|
+
mkdirSync(llmLogDir, { recursive: true });
|
|
210
|
+
const base = `${dateTimeKey()}-${sanitizeCaller(caller)}`;
|
|
211
|
+
let path = join(llmLogDir, `${base}.log`);
|
|
212
|
+
for (let n = 2;existsSync(path); n += 1) {
|
|
213
|
+
path = join(llmLogDir, `${base}-${n}.log`);
|
|
214
|
+
}
|
|
215
|
+
writeFileSync(path, content, "utf8");
|
|
216
|
+
}
|
|
155
217
|
|
|
156
218
|
// src/config/loader.ts
|
|
157
|
-
import { dirname as
|
|
219
|
+
import { dirname, join as join2, resolve } from "path";
|
|
158
220
|
import { z, ZodError } from "zod";
|
|
159
221
|
import { homedir } from "os";
|
|
160
222
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
161
|
-
import { mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
162
|
-
var brainboxRoot = process.env["BRAINBOX_ROOT_PATH"] ? resolve(process.cwd(), process.env["BRAINBOX_ROOT_PATH"]) :
|
|
223
|
+
import { mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
224
|
+
var brainboxRoot = process.env["BRAINBOX_ROOT_PATH"] ? resolve(process.cwd(), process.env["BRAINBOX_ROOT_PATH"]) : join2(homedir(), ".brainbox");
|
|
163
225
|
function configFile(file, options) {
|
|
164
|
-
const path = () =>
|
|
226
|
+
const path = () => join2(brainboxRoot, file);
|
|
165
227
|
const read = () => {
|
|
166
228
|
const p = path();
|
|
167
229
|
let raw;
|
|
@@ -173,8 +235,8 @@ function configFile(file, options) {
|
|
|
173
235
|
const defaults = defaultsFromSchema(options.schema);
|
|
174
236
|
const templateStr = (options.header ? options.header + `
|
|
175
237
|
` : "") + (defaults === undefined ? "" : stringifyYaml(defaults));
|
|
176
|
-
mkdirSync2(
|
|
177
|
-
|
|
238
|
+
mkdirSync2(dirname(p), { recursive: true });
|
|
239
|
+
writeFileSync2(p, templateStr);
|
|
178
240
|
raw = defaults ?? {};
|
|
179
241
|
}
|
|
180
242
|
try {
|
|
@@ -191,8 +253,8 @@ function configFile(file, options) {
|
|
|
191
253
|
};
|
|
192
254
|
const write = (value) => {
|
|
193
255
|
const p = path();
|
|
194
|
-
mkdirSync2(
|
|
195
|
-
|
|
256
|
+
mkdirSync2(dirname(p), { recursive: true });
|
|
257
|
+
writeFileSync2(p, stringifyYaml(value));
|
|
196
258
|
};
|
|
197
259
|
const update = (fn) => {
|
|
198
260
|
const next = fn(read());
|
|
@@ -313,7 +375,7 @@ function registerCommand(program, config2) {
|
|
|
313
375
|
|
|
314
376
|
// src/brain/manager.ts
|
|
315
377
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
316
|
-
import { join as
|
|
378
|
+
import { join as join3 } from "path";
|
|
317
379
|
var log = logger.child("brain-manager");
|
|
318
380
|
|
|
319
381
|
class BrainDBManager {
|
|
@@ -322,7 +384,7 @@ class BrainDBManager {
|
|
|
322
384
|
this.root = root;
|
|
323
385
|
}
|
|
324
386
|
dbFile() {
|
|
325
|
-
return
|
|
387
|
+
return join3(this.root, "brains.json");
|
|
326
388
|
}
|
|
327
389
|
async readDB() {
|
|
328
390
|
try {
|
|
@@ -427,15 +489,100 @@ function stripThinkTags(content) {
|
|
|
427
489
|
}
|
|
428
490
|
function parseModelJson(content) {
|
|
429
491
|
const cleaned = stripThinkTags(content);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
492
|
+
const candidates = [cleaned];
|
|
493
|
+
const fence = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
494
|
+
if (fence?.[1])
|
|
495
|
+
candidates.push(fence[1].trim());
|
|
496
|
+
const extracted = extractJsonSlice(cleaned);
|
|
497
|
+
if (extracted)
|
|
498
|
+
candidates.push(extracted);
|
|
499
|
+
let lastErr;
|
|
500
|
+
for (const candidate of candidates) {
|
|
501
|
+
try {
|
|
502
|
+
return decodeJsonValue(candidate);
|
|
503
|
+
} catch (err) {
|
|
504
|
+
lastErr = err;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
throw lastErr instanceof Error ? lastErr : new Error("Failed to parse model JSON");
|
|
508
|
+
}
|
|
509
|
+
function decodeJsonValue(text) {
|
|
510
|
+
let value = JSON.parse(text);
|
|
511
|
+
for (let i = 0;i < 2 && typeof value === "string"; i++) {
|
|
512
|
+
const inner = value.trim();
|
|
513
|
+
if (!(inner.startsWith("{") && inner.endsWith("}") || inner.startsWith("[") && inner.endsWith("]") || inner.startsWith('"') && inner.endsWith('"'))) {
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
value = JSON.parse(inner);
|
|
517
|
+
}
|
|
518
|
+
return value;
|
|
519
|
+
}
|
|
520
|
+
function extractJsonSlice(text) {
|
|
521
|
+
const start = text.search(/[\{\[]/);
|
|
522
|
+
if (start < 0)
|
|
523
|
+
return;
|
|
524
|
+
const open = text[start];
|
|
525
|
+
const close = open === "{" ? "}" : "]";
|
|
526
|
+
let depth = 0;
|
|
527
|
+
let inString = false;
|
|
528
|
+
let escape = false;
|
|
529
|
+
for (let i = start;i < text.length; i++) {
|
|
530
|
+
const ch = text[i];
|
|
531
|
+
if (inString) {
|
|
532
|
+
if (escape) {
|
|
533
|
+
escape = false;
|
|
534
|
+
} else if (ch === "\\") {
|
|
535
|
+
escape = true;
|
|
536
|
+
} else if (ch === '"') {
|
|
537
|
+
inString = false;
|
|
538
|
+
}
|
|
539
|
+
continue;
|
|
436
540
|
}
|
|
437
|
-
|
|
541
|
+
if (ch === '"') {
|
|
542
|
+
inString = true;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (ch === open)
|
|
546
|
+
depth++;
|
|
547
|
+
else if (ch === close) {
|
|
548
|
+
depth--;
|
|
549
|
+
if (depth === 0)
|
|
550
|
+
return text.slice(start, i + 1);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
function schemaToolName(name) {
|
|
556
|
+
const cleaned = name.replace(/[^a-zA-Z0-9_]/g, "_").replace(/^(\d)/, "_$1");
|
|
557
|
+
return cleaned.length > 0 ? cleaned : "submit_result";
|
|
558
|
+
}
|
|
559
|
+
function buildStructuredJsonRequest(options) {
|
|
560
|
+
const toolName = schemaToolName(options.jsonSchemaName);
|
|
561
|
+
return {
|
|
562
|
+
toolName,
|
|
563
|
+
tool: {
|
|
564
|
+
name: toolName,
|
|
565
|
+
description: `Submit the structured ${options.jsonSchemaName} result.`,
|
|
566
|
+
parameters: options.jsonSchema ?? {
|
|
567
|
+
type: "object",
|
|
568
|
+
additionalProperties: true
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
instruction: `${options.instruction}
|
|
572
|
+
|
|
573
|
+
You MUST call the \`${toolName}\` tool exactly once with the complete answer.
|
|
574
|
+
Do not write the JSON as plain text or inside a markdown code fence.`
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
function parseStructuredJsonResult(choice, toolName) {
|
|
578
|
+
const call = choice.message.toolCalls?.find((c) => c.function.name === toolName) ?? choice.message.toolCalls?.[0];
|
|
579
|
+
if (call?.function.arguments) {
|
|
580
|
+
return parseModelJson(call.function.arguments);
|
|
581
|
+
}
|
|
582
|
+
if (choice.message.content) {
|
|
583
|
+
return parseModelJson(choice.message.content);
|
|
438
584
|
}
|
|
585
|
+
throw new Error("Empty response from model");
|
|
439
586
|
}
|
|
440
587
|
function readAuthString(auth, key, envName) {
|
|
441
588
|
const fromAuth = typeof auth?.[key] === "string" ? auth[key] : undefined;
|
|
@@ -450,6 +597,56 @@ function readAuthString(auth, key, envName) {
|
|
|
450
597
|
}
|
|
451
598
|
|
|
452
599
|
class LLMExecutor {
|
|
600
|
+
async chatWithToolExecution(model, options) {
|
|
601
|
+
const messages = options.messages.slice();
|
|
602
|
+
const maxSteps = options.maxSteps ?? 20;
|
|
603
|
+
let last;
|
|
604
|
+
for (let step = 0;step < maxSteps; step += 1) {
|
|
605
|
+
log2.debug(`chatWithToolExecution: step ${step + 1}/${maxSteps} msgs=${messages.length}`);
|
|
606
|
+
last = await this.chatWithTools(model, {
|
|
607
|
+
instruction: options.instruction,
|
|
608
|
+
messages,
|
|
609
|
+
tools: options.tools,
|
|
610
|
+
caller: options.caller,
|
|
611
|
+
reasoningEffort: options.reasoningEffort,
|
|
612
|
+
parallelToolCalls: options.parallelToolCalls,
|
|
613
|
+
toolChoice: options.toolChoice
|
|
614
|
+
});
|
|
615
|
+
const toolCalls = last.message.toolCalls ?? [];
|
|
616
|
+
log2.debug(`chatWithToolExecution: step ${step + 1} toolCalls=${toolCalls.length}`);
|
|
617
|
+
if (toolCalls.length === 0) {
|
|
618
|
+
const nudge = options.onNoToolCalls ? await options.onNoToolCalls(last) : null;
|
|
619
|
+
if (nudge == null)
|
|
620
|
+
return last;
|
|
621
|
+
log2.debug(`chatWithToolExecution: bare end rejected, re-prompting`);
|
|
622
|
+
messages.push({
|
|
623
|
+
role: "assistant",
|
|
624
|
+
content: last.message.content ?? ""
|
|
625
|
+
});
|
|
626
|
+
messages.push({ role: "user", content: nudge });
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
messages.push({
|
|
630
|
+
role: "assistant",
|
|
631
|
+
content: last.message.content,
|
|
632
|
+
toolCalls
|
|
633
|
+
});
|
|
634
|
+
for (const call of toolCalls) {
|
|
635
|
+
const content = await options.executeTool(call);
|
|
636
|
+
messages.push({
|
|
637
|
+
role: "tool",
|
|
638
|
+
toolCallId: call.id,
|
|
639
|
+
content
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
if (options.shouldEnd?.(toolCalls)) {
|
|
643
|
+
log2.debug(`chatWithToolExecution: shouldEnd after step ${step + 1}`);
|
|
644
|
+
return last;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
log2.warn(`chatWithToolExecution: reached maxSteps (${maxSteps}) without final reply`);
|
|
648
|
+
return last;
|
|
649
|
+
}
|
|
453
650
|
static providers = [];
|
|
454
651
|
static registerProvider(p) {
|
|
455
652
|
LLMExecutor.providers.push(p);
|
|
@@ -516,6 +713,19 @@ class LLMExecutor {
|
|
|
516
713
|
};
|
|
517
714
|
}
|
|
518
715
|
}
|
|
716
|
+
function resolveLlmCaller(options, fallback = "llm") {
|
|
717
|
+
return options.caller ?? options.jsonSchemaName ?? fallback;
|
|
718
|
+
}
|
|
719
|
+
function logLlmWire(caller, request, response) {
|
|
720
|
+
if (!isLlmLogEnabled())
|
|
721
|
+
return;
|
|
722
|
+
writeLlmExchange(caller, `======== REQUEST ========
|
|
723
|
+
${request}
|
|
724
|
+
|
|
725
|
+
======== RESPONSE ========
|
|
726
|
+
${response}
|
|
727
|
+
`);
|
|
728
|
+
}
|
|
519
729
|
function listProviderNames() {
|
|
520
730
|
return LLMExecutor.listProviderNames();
|
|
521
731
|
}
|
|
@@ -584,27 +794,27 @@ class OpenRouterExecutor extends LLMExecutor {
|
|
|
584
794
|
async call(model, options) {
|
|
585
795
|
const jsonMode = "jsonSchemaName" in options;
|
|
586
796
|
log3.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
797
|
+
const chatRequest = {
|
|
798
|
+
model,
|
|
799
|
+
messages: [
|
|
800
|
+
{ role: "system", content: options.instruction },
|
|
801
|
+
{ role: "user", content: options.message }
|
|
802
|
+
],
|
|
803
|
+
reasoning: {
|
|
804
|
+
effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
|
|
805
|
+
},
|
|
806
|
+
responseFormat: jsonMode ? {
|
|
807
|
+
type: "json_schema",
|
|
808
|
+
jsonSchema: {
|
|
809
|
+
name: options.jsonSchemaName,
|
|
810
|
+
schema: options.jsonSchema,
|
|
811
|
+
strict: true
|
|
812
|
+
}
|
|
813
|
+
} : { type: "text" },
|
|
814
|
+
stream: false
|
|
815
|
+
};
|
|
816
|
+
const result = await this.client.chat.send({ chatRequest });
|
|
817
|
+
logLlmWire(resolveLlmCaller(options), JSON.stringify(chatRequest), JSON.stringify(result));
|
|
608
818
|
const raw = result.choices[0]?.message?.content;
|
|
609
819
|
const content = typeof raw === "string" ? stripThinkTags(raw) : raw;
|
|
610
820
|
if (!content) {
|
|
@@ -616,22 +826,22 @@ class OpenRouterExecutor extends LLMExecutor {
|
|
|
616
826
|
}
|
|
617
827
|
async chatWithTools(model, options) {
|
|
618
828
|
log3.debug(`chatWithTools: model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
829
|
+
const chatRequest = {
|
|
830
|
+
model,
|
|
831
|
+
messages: [
|
|
832
|
+
{ role: "system", content: options.instruction },
|
|
833
|
+
...options.messages.map(toOrMessage)
|
|
834
|
+
],
|
|
835
|
+
reasoning: {
|
|
836
|
+
effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
|
|
837
|
+
},
|
|
838
|
+
responseFormat: { type: "text" },
|
|
839
|
+
tools: options.tools.map(toOrTool),
|
|
840
|
+
parallelToolCalls: options.parallelToolCalls ?? false,
|
|
841
|
+
stream: false
|
|
842
|
+
};
|
|
843
|
+
const result = await this.client.chat.send({ chatRequest });
|
|
844
|
+
logLlmWire(resolveLlmCaller(options), JSON.stringify(chatRequest), JSON.stringify(result));
|
|
635
845
|
const choice = result.choices[0];
|
|
636
846
|
if (!choice) {
|
|
637
847
|
log3.debug(`chatWithTools: no choice in response`);
|
|
@@ -706,6 +916,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
|
|
|
706
916
|
chatPath;
|
|
707
917
|
noBearerPrefix;
|
|
708
918
|
reasoningEffortInQuery;
|
|
919
|
+
supportsResponseFormat;
|
|
709
920
|
constructor(opts) {
|
|
710
921
|
super();
|
|
711
922
|
this.providerName = opts.providerName;
|
|
@@ -715,6 +926,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
|
|
|
715
926
|
this.chatPath = opts.chatPath ?? "/chat/completions";
|
|
716
927
|
this.noBearerPrefix = opts.noBearerPrefix ?? false;
|
|
717
928
|
this.reasoningEffortInQuery = opts.reasoningEffortInQuery ?? false;
|
|
929
|
+
this.supportsResponseFormat = opts.supportsResponseFormat ?? true;
|
|
718
930
|
this.models = {
|
|
719
931
|
conversation: opts.conversationModel,
|
|
720
932
|
identity: opts.identityModel
|
|
@@ -745,7 +957,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
|
|
|
745
957
|
}
|
|
746
958
|
return url.toString();
|
|
747
959
|
}
|
|
748
|
-
async sendRequest(body, reasoningEffort) {
|
|
960
|
+
async sendRequest(body, reasoningEffort, caller) {
|
|
749
961
|
const modelName = body["model"];
|
|
750
962
|
const modelStr = typeof modelName === "string" ? modelName : "";
|
|
751
963
|
const url = this.buildRequestUrl(modelStr, reasoningEffort);
|
|
@@ -755,24 +967,40 @@ class OpenAICompatibleExecutor extends LLMExecutor {
|
|
|
755
967
|
...authHeader,
|
|
756
968
|
...this.defaultHeaders
|
|
757
969
|
};
|
|
970
|
+
const requestRaw = JSON.stringify(body);
|
|
758
971
|
const res = await fetch(url, {
|
|
759
972
|
method: "POST",
|
|
760
973
|
headers,
|
|
761
|
-
body:
|
|
974
|
+
body: requestRaw
|
|
762
975
|
});
|
|
976
|
+
const responseRaw = await res.text().catch(() => "");
|
|
977
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
763
978
|
if (!res.ok) {
|
|
764
|
-
|
|
765
|
-
log4.error(`${this.providerName}: HTTP ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
|
|
979
|
+
log4.error(`${this.providerName}: HTTP ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
|
|
766
980
|
throw new Error(`${this.providerName} request failed: ${res.status} ${res.statusText}`);
|
|
767
981
|
}
|
|
768
|
-
|
|
982
|
+
let data;
|
|
983
|
+
try {
|
|
984
|
+
data = JSON.parse(responseRaw);
|
|
985
|
+
} catch {
|
|
986
|
+
throw new Error(`${this.providerName}: invalid JSON response`);
|
|
987
|
+
}
|
|
769
988
|
if (data.error) {
|
|
770
989
|
log4.error(`${this.providerName}: API error ${data.error.type ?? ""} ${data.error.message ?? ""}`);
|
|
771
990
|
throw new Error(`${this.providerName} API error: ${data.error.message ?? "unknown"}`);
|
|
772
991
|
}
|
|
992
|
+
const baseCode = data.base_resp?.status_code;
|
|
993
|
+
if (typeof baseCode === "number" && baseCode !== 0) {
|
|
994
|
+
const msg = data.base_resp?.status_msg?.trim() || `status_code ${baseCode}`;
|
|
995
|
+
log4.error(`${this.providerName}: base_resp ${baseCode} ${msg}`);
|
|
996
|
+
throw new Error(`${this.providerName} API error: ${msg}`);
|
|
997
|
+
}
|
|
773
998
|
return data;
|
|
774
999
|
}
|
|
775
1000
|
async call(model, options) {
|
|
1001
|
+
if ("jsonSchemaName" in options && !this.supportsResponseFormat) {
|
|
1002
|
+
return this.callJsonViaTool(model, options);
|
|
1003
|
+
}
|
|
776
1004
|
const jsonMode = "jsonSchemaName" in options;
|
|
777
1005
|
const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
|
|
778
1006
|
log4.debug(`call: provider=${this.providerName} model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
|
|
@@ -785,16 +1013,32 @@ class OpenAICompatibleExecutor extends LLMExecutor {
|
|
|
785
1013
|
responseFormat: jsonMode ? buildResponseFormat(options.jsonSchemaName, options.jsonSchema) : undefined,
|
|
786
1014
|
reasoningEffort: reasoning
|
|
787
1015
|
});
|
|
788
|
-
const data = await this.sendRequest(body, options.reasoningEffort);
|
|
789
|
-
const
|
|
1016
|
+
const data = await this.sendRequest(body, options.reasoningEffort, resolveLlmCaller(options));
|
|
1017
|
+
const choice = data.choices?.[0];
|
|
1018
|
+
const raw = choice?.message?.content;
|
|
790
1019
|
const content = typeof raw === "string" ? stripThinkTags(raw) : raw;
|
|
791
1020
|
if (!content) {
|
|
792
|
-
|
|
793
|
-
|
|
1021
|
+
const finish = choice?.finish_reason ?? "no-choice";
|
|
1022
|
+
const reasoningLen = typeof choice?.message?.reasoning_content === "string" ? choice.message.reasoning_content.length : 0;
|
|
1023
|
+
log4.debug(`call: empty content in choice 0 finish_reason=${finish} reasoning_len=${reasoningLen} rawType=${raw === null ? "null" : typeof raw}`);
|
|
1024
|
+
throw new Error(reasoningLen > 0 ? `Empty response from model (finish_reason=${finish}; reasoning present but no content)` : "Empty response from model");
|
|
794
1025
|
}
|
|
795
1026
|
log4.debug(`call: response ${content.length} chars`);
|
|
796
1027
|
return jsonMode ? parseModelJson(content) : content;
|
|
797
1028
|
}
|
|
1029
|
+
async callJsonViaTool(model, options) {
|
|
1030
|
+
const { toolName, tool, instruction } = buildStructuredJsonRequest(options);
|
|
1031
|
+
log4.debug(`callJsonViaTool: provider=${this.providerName} model=${model} tool=${toolName}`);
|
|
1032
|
+
const choice = await this.chatWithTools(model, {
|
|
1033
|
+
caller: options.caller ?? options.jsonSchemaName,
|
|
1034
|
+
instruction,
|
|
1035
|
+
messages: [{ role: "user", content: options.message }],
|
|
1036
|
+
tools: [tool],
|
|
1037
|
+
reasoningEffort: "none",
|
|
1038
|
+
parallelToolCalls: false
|
|
1039
|
+
});
|
|
1040
|
+
return parseStructuredJsonResult(choice, toolName);
|
|
1041
|
+
}
|
|
798
1042
|
async chatWithTools(model, options) {
|
|
799
1043
|
const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
|
|
800
1044
|
log4.debug(`chatWithTools: provider=${this.providerName} model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
|
|
@@ -808,7 +1052,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
|
|
|
808
1052
|
parallelToolCalls: options.parallelToolCalls ?? false,
|
|
809
1053
|
reasoningEffort: reasoning
|
|
810
1054
|
});
|
|
811
|
-
const data = await this.sendRequest(body, options.reasoningEffort);
|
|
1055
|
+
const data = await this.sendRequest(body, options.reasoningEffort, resolveLlmCaller(options));
|
|
812
1056
|
const choice = data.choices?.[0];
|
|
813
1057
|
if (!choice) {
|
|
814
1058
|
log4.debug(`chatWithTools: no choice in response`);
|
|
@@ -1110,10 +1354,15 @@ class ZenMuxExecutor extends OpenAICompatibleExecutor {
|
|
|
1110
1354
|
// src/provider/providers/minimax.ts
|
|
1111
1355
|
class MiniMaxBase extends OpenAICompatibleExecutor {
|
|
1112
1356
|
buildBody(opts) {
|
|
1357
|
+
const jsonMode = opts.responseFormat !== undefined;
|
|
1113
1358
|
const body = super.buildBody(opts);
|
|
1114
1359
|
delete body["reasoning_effort"];
|
|
1360
|
+
delete body["response_format"];
|
|
1361
|
+
delete body["parallel_tool_calls"];
|
|
1115
1362
|
body["reasoning_split"] = true;
|
|
1116
|
-
body["
|
|
1363
|
+
body["max_completion_tokens"] = 16384;
|
|
1364
|
+
const wantThink = !jsonMode && opts.reasoningEffort !== undefined && opts.reasoningEffort !== "none";
|
|
1365
|
+
body["thinking"] = wantThink ? { type: "adaptive" } : { type: "disabled" };
|
|
1117
1366
|
return body;
|
|
1118
1367
|
}
|
|
1119
1368
|
}
|
|
@@ -1123,7 +1372,8 @@ function minimaxOpts(providerName, baseURL, opts) {
|
|
|
1123
1372
|
baseURL,
|
|
1124
1373
|
apiKey: opts.apiKey,
|
|
1125
1374
|
conversationModel: opts.conversationModel,
|
|
1126
|
-
identityModel: opts.identityModel
|
|
1375
|
+
identityModel: opts.identityModel,
|
|
1376
|
+
supportsResponseFormat: false
|
|
1127
1377
|
};
|
|
1128
1378
|
}
|
|
1129
1379
|
|
|
@@ -1199,7 +1449,8 @@ class LmStudioExecutor extends OpenAICompatibleExecutor {
|
|
|
1199
1449
|
baseURL: "http://127.0.0.1:1234/v1",
|
|
1200
1450
|
apiKey: opts.apiKey || "lm-studio",
|
|
1201
1451
|
conversationModel: opts.conversationModel,
|
|
1202
|
-
identityModel: opts.identityModel
|
|
1452
|
+
identityModel: opts.identityModel,
|
|
1453
|
+
supportsResponseFormat: false
|
|
1203
1454
|
});
|
|
1204
1455
|
}
|
|
1205
1456
|
}
|
|
@@ -1212,7 +1463,8 @@ class OllamaExecutor extends OpenAICompatibleExecutor {
|
|
|
1212
1463
|
baseURL: "http://127.0.0.1:11434/v1",
|
|
1213
1464
|
apiKey: opts.apiKey || "ollama",
|
|
1214
1465
|
conversationModel: opts.conversationModel,
|
|
1215
|
-
identityModel: opts.identityModel
|
|
1466
|
+
identityModel: opts.identityModel,
|
|
1467
|
+
supportsResponseFormat: false
|
|
1216
1468
|
});
|
|
1217
1469
|
}
|
|
1218
1470
|
}
|
|
@@ -1238,7 +1490,8 @@ class LlamaCppExecutor extends OpenAICompatibleExecutor {
|
|
|
1238
1490
|
baseURL: "http://127.0.0.1:8080/v1",
|
|
1239
1491
|
apiKey: opts.apiKey || "llama.cpp",
|
|
1240
1492
|
conversationModel: opts.conversationModel,
|
|
1241
|
-
identityModel: opts.identityModel
|
|
1493
|
+
identityModel: opts.identityModel,
|
|
1494
|
+
supportsResponseFormat: false
|
|
1242
1495
|
});
|
|
1243
1496
|
}
|
|
1244
1497
|
}
|
|
@@ -1321,21 +1574,23 @@ class CloudflareWorkersExecutor extends LLMExecutor {
|
|
|
1321
1574
|
const accountId = readAuthString(opts.auth, "accountId", "CLOUDFLARE_ACCOUNT_ID");
|
|
1322
1575
|
this.baseURL = accountId ? `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run` : "https://api.cloudflare.com/client/v4/accounts/__cf_account_id__/ai/run";
|
|
1323
1576
|
}
|
|
1324
|
-
async run(model, body) {
|
|
1577
|
+
async run(model, body, caller) {
|
|
1325
1578
|
const url = `${this.baseURL}/${encodeURIComponent(model)}`;
|
|
1579
|
+
const requestRaw = JSON.stringify(body);
|
|
1326
1580
|
const res = await fetch(url, {
|
|
1327
1581
|
method: "POST",
|
|
1328
1582
|
headers: {
|
|
1329
1583
|
Authorization: `Bearer ${this.apiKey}`,
|
|
1330
1584
|
"Content-Type": "application/json"
|
|
1331
1585
|
},
|
|
1332
|
-
body:
|
|
1586
|
+
body: requestRaw
|
|
1333
1587
|
});
|
|
1588
|
+
const responseRaw = await res.text().catch(() => "");
|
|
1589
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
1334
1590
|
if (!res.ok) {
|
|
1335
|
-
|
|
1336
|
-
throw new Error(`cloudflare-workers request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
|
|
1591
|
+
throw new Error(`cloudflare-workers request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
|
|
1337
1592
|
}
|
|
1338
|
-
return
|
|
1593
|
+
return JSON.parse(responseRaw);
|
|
1339
1594
|
}
|
|
1340
1595
|
async call(model, options) {
|
|
1341
1596
|
const jsonMode = "jsonSchemaName" in options;
|
|
@@ -1356,7 +1611,7 @@ class CloudflareWorkersExecutor extends LLMExecutor {
|
|
|
1356
1611
|
if (reasoning !== "none") {
|
|
1357
1612
|
body["reasoning_effort"] = reasoning;
|
|
1358
1613
|
}
|
|
1359
|
-
const data = await this.run(model, body);
|
|
1614
|
+
const data = await this.run(model, body, resolveLlmCaller(options));
|
|
1360
1615
|
if (data.errors && data.errors.length > 0) {
|
|
1361
1616
|
throw new Error(`cloudflare-workers API error: ${data.errors.map((e) => e.message).join("; ")}`);
|
|
1362
1617
|
}
|
|
@@ -1385,7 +1640,7 @@ class CloudflareWorkersExecutor extends LLMExecutor {
|
|
|
1385
1640
|
if (reasoning !== "none") {
|
|
1386
1641
|
body["reasoning_effort"] = reasoning;
|
|
1387
1642
|
}
|
|
1388
|
-
const data = await this.run(model, body);
|
|
1643
|
+
const data = await this.run(model, body, resolveLlmCaller(options));
|
|
1389
1644
|
if (data.errors && data.errors.length > 0) {
|
|
1390
1645
|
throw new Error(`cloudflare-workers API error: ${data.errors.map((e) => e.message).join("; ")}`);
|
|
1391
1646
|
}
|
|
@@ -1475,17 +1730,17 @@ var AnthropicAuthSchema = z4.object({
|
|
|
1475
1730
|
apiVersion: z4.string().optional()
|
|
1476
1731
|
}).loose();
|
|
1477
1732
|
function toAnthropicMessages(messages) {
|
|
1478
|
-
let
|
|
1479
|
-
const
|
|
1733
|
+
let system2;
|
|
1734
|
+
const msgs2 = [];
|
|
1480
1735
|
for (const m of messages) {
|
|
1481
1736
|
if (m.role === "system") {
|
|
1482
|
-
|
|
1737
|
+
system2 = (system2 ? system2 + `
|
|
1483
1738
|
|
|
1484
1739
|
` : "") + m.content;
|
|
1485
1740
|
continue;
|
|
1486
1741
|
}
|
|
1487
1742
|
if (m.role === "user") {
|
|
1488
|
-
|
|
1743
|
+
msgs2.push({ role: "user", content: m.content });
|
|
1489
1744
|
continue;
|
|
1490
1745
|
}
|
|
1491
1746
|
if (m.role === "assistant") {
|
|
@@ -1508,11 +1763,11 @@ function toAnthropicMessages(messages) {
|
|
|
1508
1763
|
});
|
|
1509
1764
|
}
|
|
1510
1765
|
}
|
|
1511
|
-
|
|
1766
|
+
msgs2.push({ role: "assistant", content: blocks });
|
|
1512
1767
|
continue;
|
|
1513
1768
|
}
|
|
1514
1769
|
if (m.role === "tool") {
|
|
1515
|
-
|
|
1770
|
+
msgs2.push({
|
|
1516
1771
|
role: "user",
|
|
1517
1772
|
content: [
|
|
1518
1773
|
{
|
|
@@ -1524,7 +1779,7 @@ function toAnthropicMessages(messages) {
|
|
|
1524
1779
|
});
|
|
1525
1780
|
}
|
|
1526
1781
|
}
|
|
1527
|
-
return { system, msgs };
|
|
1782
|
+
return { system: system2, msgs: msgs2 };
|
|
1528
1783
|
}
|
|
1529
1784
|
function toAnthropicTool(t) {
|
|
1530
1785
|
return {
|
|
@@ -1552,7 +1807,8 @@ class AnthropicExecutor extends LLMExecutor {
|
|
|
1552
1807
|
this.baseURL = (extra.baseURL ?? readAuthString(opts.auth, "baseURL", "ANTHROPIC_BASE_URL") ?? "https://api.anthropic.com").replace(/\/v1\/?$/, "");
|
|
1553
1808
|
this.apiVersion = extra.apiVersion ?? "2023-06-01";
|
|
1554
1809
|
}
|
|
1555
|
-
async send(body) {
|
|
1810
|
+
async send(body, caller) {
|
|
1811
|
+
const requestRaw = JSON.stringify(body);
|
|
1556
1812
|
const res = await fetch(`${this.baseURL}/v1/messages`, {
|
|
1557
1813
|
method: "POST",
|
|
1558
1814
|
headers: {
|
|
@@ -1560,19 +1816,35 @@ class AnthropicExecutor extends LLMExecutor {
|
|
|
1560
1816
|
"anthropic-version": this.apiVersion,
|
|
1561
1817
|
"Content-Type": "application/json"
|
|
1562
1818
|
},
|
|
1563
|
-
body:
|
|
1819
|
+
body: requestRaw
|
|
1564
1820
|
});
|
|
1821
|
+
const responseRaw = await res.text().catch(() => "");
|
|
1822
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
1565
1823
|
if (!res.ok) {
|
|
1566
|
-
|
|
1567
|
-
throw new Error(`anthropic request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
|
|
1824
|
+
throw new Error(`anthropic request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
|
|
1568
1825
|
}
|
|
1569
|
-
return
|
|
1826
|
+
return JSON.parse(responseRaw);
|
|
1570
1827
|
}
|
|
1571
1828
|
async call(model, options) {
|
|
1572
|
-
|
|
1829
|
+
if ("jsonSchemaName" in options) {
|
|
1830
|
+
const { toolName, tool, instruction } = buildStructuredJsonRequest({
|
|
1831
|
+
instruction: options.instruction,
|
|
1832
|
+
jsonSchemaName: options.jsonSchemaName,
|
|
1833
|
+
jsonSchema: options.jsonSchema
|
|
1834
|
+
});
|
|
1835
|
+
const choice = await this.chatWithTools(model, {
|
|
1836
|
+
caller: options.caller ?? options.jsonSchemaName,
|
|
1837
|
+
instruction,
|
|
1838
|
+
messages: [{ role: "user", content: options.message }],
|
|
1839
|
+
tools: [tool],
|
|
1840
|
+
reasoningEffort: "none",
|
|
1841
|
+
toolChoice: { type: "tool", name: toolName }
|
|
1842
|
+
});
|
|
1843
|
+
return parseStructuredJsonResult(choice, toolName);
|
|
1844
|
+
}
|
|
1573
1845
|
const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
|
|
1574
1846
|
const log5 = logger.child("llm:anthropic");
|
|
1575
|
-
log5.debug(`call: model=${model} jsonSchema
|
|
1847
|
+
log5.debug(`call: model=${model} jsonSchema=- msgLen=${options.message.length}`);
|
|
1576
1848
|
const outputCap = 4096;
|
|
1577
1849
|
const body = {
|
|
1578
1850
|
model,
|
|
@@ -1588,7 +1860,7 @@ class AnthropicExecutor extends LLMExecutor {
|
|
|
1588
1860
|
budget_tokens: budget
|
|
1589
1861
|
};
|
|
1590
1862
|
}
|
|
1591
|
-
const data = await this.send(body);
|
|
1863
|
+
const data = await this.send(body, resolveLlmCaller(options));
|
|
1592
1864
|
if (data.error) {
|
|
1593
1865
|
throw new Error(`anthropic API error: ${data.error.message ?? "unknown"}`);
|
|
1594
1866
|
}
|
|
@@ -1596,21 +1868,27 @@ class AnthropicExecutor extends LLMExecutor {
|
|
|
1596
1868
|
if (!text) {
|
|
1597
1869
|
throw new Error("Empty response from model");
|
|
1598
1870
|
}
|
|
1599
|
-
return
|
|
1871
|
+
return text;
|
|
1600
1872
|
}
|
|
1601
1873
|
async chatWithTools(model, options) {
|
|
1602
1874
|
const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
|
|
1603
1875
|
const log5 = logger.child("llm:anthropic");
|
|
1604
1876
|
log5.debug(`chatWithTools: model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
|
|
1605
|
-
const { system, msgs } = toAnthropicMessages(options.messages);
|
|
1877
|
+
const { system: system2, msgs: msgs2 } = toAnthropicMessages(options.messages);
|
|
1606
1878
|
const outputCap = 4096;
|
|
1607
1879
|
const body = {
|
|
1608
1880
|
model,
|
|
1609
1881
|
max_tokens: outputCap,
|
|
1610
|
-
system:
|
|
1611
|
-
messages:
|
|
1882
|
+
system: system2 ?? options.instruction,
|
|
1883
|
+
messages: msgs2,
|
|
1612
1884
|
tools: options.tools.map(toAnthropicTool)
|
|
1613
1885
|
};
|
|
1886
|
+
if (options.toolChoice) {
|
|
1887
|
+
body["tool_choice"] = {
|
|
1888
|
+
type: "tool",
|
|
1889
|
+
name: options.toolChoice.name
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1614
1892
|
if (reasoning !== "none") {
|
|
1615
1893
|
const budget = REASONING_BUDGET[reasoning];
|
|
1616
1894
|
body["max_tokens"] = budget + outputCap;
|
|
@@ -1619,7 +1897,7 @@ class AnthropicExecutor extends LLMExecutor {
|
|
|
1619
1897
|
budget_tokens: budget
|
|
1620
1898
|
};
|
|
1621
1899
|
}
|
|
1622
|
-
const data = await this.send(body);
|
|
1900
|
+
const data = await this.send(body, resolveLlmCaller(options));
|
|
1623
1901
|
if (data.error) {
|
|
1624
1902
|
throw new Error(`anthropic API error: ${data.error.message ?? "unknown"}`);
|
|
1625
1903
|
}
|
|
@@ -1638,11 +1916,11 @@ class AnthropicExecutor extends LLMExecutor {
|
|
|
1638
1916
|
|
|
1639
1917
|
// src/provider/providers/bedrock.ts
|
|
1640
1918
|
import { createHmac, createHash } from "node:crypto";
|
|
1641
|
-
var
|
|
1919
|
+
var pad22 = (n) => n.toString().padStart(2, "0");
|
|
1642
1920
|
function amzDate(now) {
|
|
1643
1921
|
return {
|
|
1644
|
-
date: `${now.getUTCFullYear()}${
|
|
1645
|
-
datetime: `${now.getUTCFullYear()}${
|
|
1922
|
+
date: `${now.getUTCFullYear()}${pad22(now.getUTCMonth() + 1)}${pad22(now.getUTCDate())}`,
|
|
1923
|
+
datetime: `${now.getUTCFullYear()}${pad22(now.getUTCMonth() + 1)}${pad22(now.getUTCDate())}T` + `${pad22(now.getUTCHours())}${pad22(now.getUTCMinutes())}${pad22(now.getUTCSeconds())}Z`
|
|
1646
1924
|
};
|
|
1647
1925
|
}
|
|
1648
1926
|
function signRequest(opts) {
|
|
@@ -1689,58 +1967,6 @@ function signRequest(opts) {
|
|
|
1689
1967
|
headers["X-Amz-Security-Token"] = opts.sessionToken;
|
|
1690
1968
|
return headers;
|
|
1691
1969
|
}
|
|
1692
|
-
function toAnthropicMessages2(messages) {
|
|
1693
|
-
let system;
|
|
1694
|
-
const msgs = [];
|
|
1695
|
-
for (const m of messages) {
|
|
1696
|
-
if (m.role === "system") {
|
|
1697
|
-
system = (system ? system + `
|
|
1698
|
-
|
|
1699
|
-
` : "") + m.content;
|
|
1700
|
-
continue;
|
|
1701
|
-
}
|
|
1702
|
-
if (m.role === "user") {
|
|
1703
|
-
msgs.push({ role: "user", content: [{ type: "text", text: m.content }] });
|
|
1704
|
-
continue;
|
|
1705
|
-
}
|
|
1706
|
-
if (m.role === "assistant") {
|
|
1707
|
-
const blocks = [];
|
|
1708
|
-
if (m.content)
|
|
1709
|
-
blocks.push({ type: "text", text: m.content });
|
|
1710
|
-
if (m.toolCalls) {
|
|
1711
|
-
for (const c of m.toolCalls) {
|
|
1712
|
-
let input = {};
|
|
1713
|
-
try {
|
|
1714
|
-
input = c.function.arguments ? JSON.parse(c.function.arguments) : {};
|
|
1715
|
-
} catch {
|
|
1716
|
-
input = {};
|
|
1717
|
-
}
|
|
1718
|
-
blocks.push({
|
|
1719
|
-
type: "tool_use",
|
|
1720
|
-
id: c.id,
|
|
1721
|
-
name: c.function.name,
|
|
1722
|
-
input
|
|
1723
|
-
});
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
msgs.push({ role: "assistant", content: blocks });
|
|
1727
|
-
continue;
|
|
1728
|
-
}
|
|
1729
|
-
if (m.role === "tool") {
|
|
1730
|
-
msgs.push({
|
|
1731
|
-
role: "user",
|
|
1732
|
-
content: [
|
|
1733
|
-
{
|
|
1734
|
-
type: "tool_result",
|
|
1735
|
-
tool_use_id: m.toolCallId,
|
|
1736
|
-
content: [{ type: "text", text: m.content }]
|
|
1737
|
-
}
|
|
1738
|
-
]
|
|
1739
|
-
});
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
return { system, msgs };
|
|
1743
|
-
}
|
|
1744
1970
|
function toAnthropicTool2(t) {
|
|
1745
1971
|
return {
|
|
1746
1972
|
name: t.name,
|
|
@@ -1782,7 +2008,7 @@ class BedrockExecutor extends LLMExecutor {
|
|
|
1782
2008
|
this.sessionToken = readAuthString(opts.auth, "sessionToken", "AWS_SESSION_TOKEN");
|
|
1783
2009
|
this.baseURL = `https://bedrock-runtime.${this.region}.amazonaws.com`;
|
|
1784
2010
|
}
|
|
1785
|
-
async invoke(model, body) {
|
|
2011
|
+
async invoke(model, body, caller) {
|
|
1786
2012
|
const bodyStr = JSON.stringify(body);
|
|
1787
2013
|
const path = `/model/${encodeURIComponent(model)}/invoke`;
|
|
1788
2014
|
const headers = signRequest({
|
|
@@ -1801,16 +2027,32 @@ class BedrockExecutor extends LLMExecutor {
|
|
|
1801
2027
|
headers,
|
|
1802
2028
|
body: bodyStr
|
|
1803
2029
|
});
|
|
2030
|
+
const responseRaw = await res.text().catch(() => "");
|
|
2031
|
+
logLlmWire(caller, bodyStr, responseRaw);
|
|
1804
2032
|
if (!res.ok) {
|
|
1805
|
-
|
|
1806
|
-
throw new Error(`bedrock request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
|
|
2033
|
+
throw new Error(`bedrock request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
|
|
1807
2034
|
}
|
|
1808
|
-
return
|
|
2035
|
+
return JSON.parse(responseRaw);
|
|
1809
2036
|
}
|
|
1810
2037
|
async call(model, options) {
|
|
1811
|
-
const jsonMode = "jsonSchemaName" in options;
|
|
1812
2038
|
const log5 = logger.child("llm:bedrock");
|
|
1813
|
-
|
|
2039
|
+
if ("jsonSchemaName" in options) {
|
|
2040
|
+
log5.debug(`call: model=${model} jsonSchema=${options.jsonSchemaName} via tool`);
|
|
2041
|
+
const { toolName, tool, instruction } = buildStructuredJsonRequest({
|
|
2042
|
+
instruction: options.instruction,
|
|
2043
|
+
jsonSchemaName: options.jsonSchemaName,
|
|
2044
|
+
jsonSchema: options.jsonSchema
|
|
2045
|
+
});
|
|
2046
|
+
const choice = await this.chatWithTools(model, {
|
|
2047
|
+
caller: options.caller ?? options.jsonSchemaName,
|
|
2048
|
+
instruction,
|
|
2049
|
+
messages: [{ role: "user", content: options.message }],
|
|
2050
|
+
tools: [tool],
|
|
2051
|
+
toolChoice: { type: "tool", name: toolName }
|
|
2052
|
+
});
|
|
2053
|
+
return parseStructuredJsonResult(choice, toolName);
|
|
2054
|
+
}
|
|
2055
|
+
log5.debug(`call: model=${model} jsonSchema=- msgLen=${options.message.length}`);
|
|
1814
2056
|
if (!model.startsWith("anthropic.") && !model.startsWith("us.anthropic.")) {
|
|
1815
2057
|
throw new Error(`bedrock provider currently only supports Anthropic models (got ${model})`);
|
|
1816
2058
|
}
|
|
@@ -1820,12 +2062,12 @@ class BedrockExecutor extends LLMExecutor {
|
|
|
1820
2062
|
system: options.instruction,
|
|
1821
2063
|
messages: [{ role: "user", content: options.message }]
|
|
1822
2064
|
};
|
|
1823
|
-
const data = await this.invoke(model, body);
|
|
2065
|
+
const data = await this.invoke(model, body, resolveLlmCaller(options));
|
|
1824
2066
|
const { text } = extractAnthropicContent(data);
|
|
1825
2067
|
if (!text) {
|
|
1826
2068
|
throw new Error("Empty response from model");
|
|
1827
2069
|
}
|
|
1828
|
-
return
|
|
2070
|
+
return text;
|
|
1829
2071
|
}
|
|
1830
2072
|
async chatWithTools(model, options) {
|
|
1831
2073
|
const log5 = logger.child("llm:bedrock");
|
|
@@ -1833,7 +2075,6 @@ class BedrockExecutor extends LLMExecutor {
|
|
|
1833
2075
|
if (!model.startsWith("anthropic.") && !model.startsWith("us.anthropic.")) {
|
|
1834
2076
|
throw new Error(`bedrock provider currently only supports Anthropic models (got ${model})`);
|
|
1835
2077
|
}
|
|
1836
|
-
const { system, msgs } = toAnthropicMessages2(options.messages);
|
|
1837
2078
|
const body = {
|
|
1838
2079
|
anthropic_version: "bedrock-2023-05-31",
|
|
1839
2080
|
max_tokens: 4096,
|
|
@@ -1841,7 +2082,13 @@ class BedrockExecutor extends LLMExecutor {
|
|
|
1841
2082
|
messages: msgs,
|
|
1842
2083
|
tools: options.tools.map(toAnthropicTool2)
|
|
1843
2084
|
};
|
|
1844
|
-
|
|
2085
|
+
if (options.toolChoice) {
|
|
2086
|
+
body.tool_choice = {
|
|
2087
|
+
type: "tool",
|
|
2088
|
+
name: options.toolChoice.name
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
const data = await this.invoke(model, body, resolveLlmCaller(options));
|
|
1845
2092
|
const { text, toolCalls } = extractAnthropicContent(data);
|
|
1846
2093
|
return { message: { content: text || undefined, toolCalls } };
|
|
1847
2094
|
}
|
|
@@ -1957,21 +2204,23 @@ class VertexExecutor extends LLMExecutor {
|
|
|
1957
2204
|
this.region = extra.region ?? readAuthString(opts.auth, "region", "GOOGLE_CLOUD_REGION") ?? "us-central1";
|
|
1958
2205
|
this.baseURL = this.project ? `https://${this.region}-aiplatform.googleapis.com/v1/projects/${this.project}/locations/${this.region}/publishers/google/models` : "https://us-central1-aiplatform.googleapis.com/v1";
|
|
1959
2206
|
}
|
|
1960
|
-
async generate(model, body) {
|
|
2207
|
+
async generate(model, body, caller) {
|
|
1961
2208
|
const url = `${this.baseURL}/${encodeURIComponent(model)}:generateContent`;
|
|
2209
|
+
const requestRaw = JSON.stringify(body);
|
|
1962
2210
|
const res = await fetch(url, {
|
|
1963
2211
|
method: "POST",
|
|
1964
2212
|
headers: {
|
|
1965
2213
|
Authorization: `Bearer ${this.apiKey}`,
|
|
1966
2214
|
"Content-Type": "application/json"
|
|
1967
2215
|
},
|
|
1968
|
-
body:
|
|
2216
|
+
body: requestRaw
|
|
1969
2217
|
});
|
|
2218
|
+
const responseRaw = await res.text().catch(() => "");
|
|
2219
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
1970
2220
|
if (!res.ok) {
|
|
1971
|
-
|
|
1972
|
-
throw new Error(`vertex request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
|
|
2221
|
+
throw new Error(`vertex request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
|
|
1973
2222
|
}
|
|
1974
|
-
return
|
|
2223
|
+
return JSON.parse(responseRaw);
|
|
1975
2224
|
}
|
|
1976
2225
|
async call(model, options) {
|
|
1977
2226
|
const jsonMode = "jsonSchemaName" in options;
|
|
@@ -1992,7 +2241,7 @@ class VertexExecutor extends LLMExecutor {
|
|
|
1992
2241
|
responseSchema: options.jsonSchema
|
|
1993
2242
|
};
|
|
1994
2243
|
}
|
|
1995
|
-
const data = await this.generate(model, body);
|
|
2244
|
+
const data = await this.generate(model, body, resolveLlmCaller(options));
|
|
1996
2245
|
if (data.error) {
|
|
1997
2246
|
throw new Error(`vertex API error: ${data.error.message ?? "unknown"}`);
|
|
1998
2247
|
}
|
|
@@ -2011,7 +2260,7 @@ class VertexExecutor extends LLMExecutor {
|
|
|
2011
2260
|
systemInstruction: { parts: [{ text: options.instruction }] },
|
|
2012
2261
|
tools: options.tools.length > 0 ? [toGeminiTools(options.tools)] : undefined
|
|
2013
2262
|
};
|
|
2014
|
-
const data = await this.generate(model, body);
|
|
2263
|
+
const data = await this.generate(model, body, resolveLlmCaller(options));
|
|
2015
2264
|
if (data.error) {
|
|
2016
2265
|
throw new Error(`vertex API error: ${data.error.message ?? "unknown"}`);
|
|
2017
2266
|
}
|
|
@@ -2087,25 +2336,41 @@ class GitLabDuoExecutor extends LLMExecutor {
|
|
|
2087
2336
|
const extra = parsed.success ? parsed.data : {};
|
|
2088
2337
|
this.baseURL = (extra.baseURL ?? readAuthString(opts.auth, "baseURL", "GITLAB_BASE_URL") ?? "https://gitlab.com/api/v4/ai/llm/proxy").replace(/\/+$/, "");
|
|
2089
2338
|
}
|
|
2090
|
-
async send(body) {
|
|
2339
|
+
async send(body, caller) {
|
|
2340
|
+
const requestRaw = JSON.stringify(body);
|
|
2091
2341
|
const res = await fetch(this.baseURL, {
|
|
2092
2342
|
method: "POST",
|
|
2093
2343
|
headers: {
|
|
2094
2344
|
Authorization: `Bearer ${this.apiKey}`,
|
|
2095
2345
|
"Content-Type": "application/json"
|
|
2096
2346
|
},
|
|
2097
|
-
body:
|
|
2347
|
+
body: requestRaw
|
|
2098
2348
|
});
|
|
2349
|
+
const responseRaw = await res.text().catch(() => "");
|
|
2350
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
2099
2351
|
if (!res.ok) {
|
|
2100
|
-
|
|
2101
|
-
throw new Error(`gitlab-duo request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
|
|
2352
|
+
throw new Error(`gitlab-duo request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
|
|
2102
2353
|
}
|
|
2103
|
-
return
|
|
2354
|
+
return JSON.parse(responseRaw);
|
|
2104
2355
|
}
|
|
2105
2356
|
async call(model, options) {
|
|
2106
|
-
const jsonMode = "jsonSchemaName" in options;
|
|
2107
2357
|
const log5 = logger.child("llm:gitlab-duo");
|
|
2108
|
-
|
|
2358
|
+
if ("jsonSchemaName" in options) {
|
|
2359
|
+
log5.debug(`call: model=${model} jsonSchema=${options.jsonSchemaName} via tool`);
|
|
2360
|
+
const { toolName, tool, instruction } = buildStructuredJsonRequest({
|
|
2361
|
+
instruction: options.instruction,
|
|
2362
|
+
jsonSchemaName: options.jsonSchemaName,
|
|
2363
|
+
jsonSchema: options.jsonSchema
|
|
2364
|
+
});
|
|
2365
|
+
const choice = await this.chatWithTools(model, {
|
|
2366
|
+
caller: options.caller ?? options.jsonSchemaName,
|
|
2367
|
+
instruction,
|
|
2368
|
+
messages: [{ role: "user", content: options.message }],
|
|
2369
|
+
tools: [tool]
|
|
2370
|
+
});
|
|
2371
|
+
return parseStructuredJsonResult(choice, toolName);
|
|
2372
|
+
}
|
|
2373
|
+
log5.debug(`call: model=${model} jsonSchema=- msgLen=${options.message.length}`);
|
|
2109
2374
|
const body = {
|
|
2110
2375
|
model,
|
|
2111
2376
|
messages: [
|
|
@@ -2113,7 +2378,7 @@ class GitLabDuoExecutor extends LLMExecutor {
|
|
|
2113
2378
|
{ role: "user", content: options.message }
|
|
2114
2379
|
]
|
|
2115
2380
|
};
|
|
2116
|
-
const data = await this.send(body);
|
|
2381
|
+
const data = await this.send(body, resolveLlmCaller(options));
|
|
2117
2382
|
if (data.error) {
|
|
2118
2383
|
throw new Error(`gitlab-duo API error: ${data.error.message ?? "unknown"}`);
|
|
2119
2384
|
}
|
|
@@ -2121,7 +2386,7 @@ class GitLabDuoExecutor extends LLMExecutor {
|
|
|
2121
2386
|
if (!content) {
|
|
2122
2387
|
throw new Error("Empty response from model");
|
|
2123
2388
|
}
|
|
2124
|
-
return
|
|
2389
|
+
return content;
|
|
2125
2390
|
}
|
|
2126
2391
|
async chatWithTools(model, options) {
|
|
2127
2392
|
const log5 = logger.child("llm:gitlab-duo");
|
|
@@ -2134,7 +2399,7 @@ class GitLabDuoExecutor extends LLMExecutor {
|
|
|
2134
2399
|
],
|
|
2135
2400
|
tools: options.tools.map(toTool)
|
|
2136
2401
|
};
|
|
2137
|
-
const data = await this.send(body);
|
|
2402
|
+
const data = await this.send(body, resolveLlmCaller(options));
|
|
2138
2403
|
if (data.error) {
|
|
2139
2404
|
throw new Error(`gitlab-duo API error: ${data.error.message ?? "unknown"}`);
|
|
2140
2405
|
}
|
|
@@ -2202,26 +2467,43 @@ class SnowflakeCortexExecutor extends LLMExecutor {
|
|
|
2202
2467
|
const account = extra.account ?? readAuthString(opts.auth, "account", "SNOWFLAKE_ACCOUNT") ?? "";
|
|
2203
2468
|
this.baseURL = account ? `https://${account}.snowflakecomputing.com/api/v2/cortex/inference:complete` : "https://__account__.snowflakecomputing.com/api/v2/cortex/inference:complete";
|
|
2204
2469
|
}
|
|
2205
|
-
async send(body) {
|
|
2470
|
+
async send(body, caller) {
|
|
2471
|
+
const requestRaw = JSON.stringify(body);
|
|
2206
2472
|
const res = await fetch(this.baseURL, {
|
|
2207
2473
|
method: "POST",
|
|
2208
2474
|
headers: {
|
|
2209
2475
|
Authorization: `Bearer ${this.apiKey}`,
|
|
2210
2476
|
"Content-Type": "application/json"
|
|
2211
2477
|
},
|
|
2212
|
-
body:
|
|
2478
|
+
body: requestRaw
|
|
2213
2479
|
});
|
|
2480
|
+
const responseRaw = await res.text().catch(() => "");
|
|
2481
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
2214
2482
|
if (!res.ok) {
|
|
2215
|
-
|
|
2216
|
-
throw new Error(`snowflake-cortex request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
|
|
2483
|
+
throw new Error(`snowflake-cortex request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
|
|
2217
2484
|
}
|
|
2218
|
-
return
|
|
2485
|
+
return JSON.parse(responseRaw);
|
|
2219
2486
|
}
|
|
2220
2487
|
async call(model, options) {
|
|
2221
|
-
const jsonMode = "jsonSchemaName" in options;
|
|
2222
|
-
const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
|
|
2223
2488
|
const log5 = logger.child("llm:snowflake-cortex");
|
|
2224
|
-
|
|
2489
|
+
if ("jsonSchemaName" in options) {
|
|
2490
|
+
log5.debug(`call: model=${model} jsonSchema=${options.jsonSchemaName} via tool`);
|
|
2491
|
+
const { toolName, tool, instruction } = buildStructuredJsonRequest({
|
|
2492
|
+
instruction: options.instruction,
|
|
2493
|
+
jsonSchemaName: options.jsonSchemaName,
|
|
2494
|
+
jsonSchema: options.jsonSchema
|
|
2495
|
+
});
|
|
2496
|
+
const choice = await this.chatWithTools(model, {
|
|
2497
|
+
caller: options.caller ?? options.jsonSchemaName,
|
|
2498
|
+
instruction,
|
|
2499
|
+
messages: [{ role: "user", content: options.message }],
|
|
2500
|
+
tools: [tool],
|
|
2501
|
+
reasoningEffort: "none"
|
|
2502
|
+
});
|
|
2503
|
+
return parseStructuredJsonResult(choice, toolName);
|
|
2504
|
+
}
|
|
2505
|
+
const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
|
|
2506
|
+
log5.debug(`call: model=${model} jsonSchema=- msgLen=${options.message.length}`);
|
|
2225
2507
|
const body = {
|
|
2226
2508
|
model,
|
|
2227
2509
|
messages: [
|
|
@@ -2232,7 +2514,7 @@ class SnowflakeCortexExecutor extends LLMExecutor {
|
|
|
2232
2514
|
if (reasoning !== "none") {
|
|
2233
2515
|
body["reasoning_effort"] = reasoning;
|
|
2234
2516
|
}
|
|
2235
|
-
const data = await this.send(body);
|
|
2517
|
+
const data = await this.send(body, resolveLlmCaller(options));
|
|
2236
2518
|
if (data.error) {
|
|
2237
2519
|
throw new Error(`snowflake-cortex API error: ${data.error.message ?? "unknown"}`);
|
|
2238
2520
|
}
|
|
@@ -2240,7 +2522,7 @@ class SnowflakeCortexExecutor extends LLMExecutor {
|
|
|
2240
2522
|
if (!content) {
|
|
2241
2523
|
throw new Error("Empty response from model");
|
|
2242
2524
|
}
|
|
2243
|
-
return
|
|
2525
|
+
return content;
|
|
2244
2526
|
}
|
|
2245
2527
|
async chatWithTools(model, options) {
|
|
2246
2528
|
const log5 = logger.child("llm:snowflake-cortex");
|
|
@@ -2253,7 +2535,7 @@ class SnowflakeCortexExecutor extends LLMExecutor {
|
|
|
2253
2535
|
],
|
|
2254
2536
|
tools: options.tools.map(toCortexTool)
|
|
2255
2537
|
};
|
|
2256
|
-
const data = await this.send(body);
|
|
2538
|
+
const data = await this.send(body, resolveLlmCaller(options));
|
|
2257
2539
|
if (data.error) {
|
|
2258
2540
|
throw new Error(`snowflake-cortex API error: ${data.error.message ?? "unknown"}`);
|
|
2259
2541
|
}
|
|
@@ -2332,14 +2614,14 @@ var llm = new Proxy({}, {
|
|
|
2332
2614
|
// src/provider/promptLoader.ts
|
|
2333
2615
|
import { existsSync as existsSync2 } from "fs";
|
|
2334
2616
|
import { readFile as readFile2 } from "fs/promises";
|
|
2335
|
-
import path, { dirname as
|
|
2617
|
+
import path, { dirname as dirname2 } from "path";
|
|
2336
2618
|
import { fileURLToPath } from "url";
|
|
2337
2619
|
var log5 = logger.child("prompt-loader");
|
|
2338
2620
|
function fileName(promptKey) {
|
|
2339
2621
|
return promptKey.toLowerCase() + ".md";
|
|
2340
2622
|
}
|
|
2341
2623
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2342
|
-
var __dirname2 =
|
|
2624
|
+
var __dirname2 = dirname2(__filename2);
|
|
2343
2625
|
var PROMPTS_DIR = existsSync2(path.resolve(__dirname2, "prompts")) ? path.resolve(__dirname2, "prompts") : path.resolve(__dirname2, "../../prompts");
|
|
2344
2626
|
async function loadPrompt(promptKey) {
|
|
2345
2627
|
const filePath = path.join(PROMPTS_DIR, fileName(promptKey));
|
|
@@ -2464,14 +2746,14 @@ function translateMessageHistory(personaName, entries) {
|
|
|
2464
2746
|
}
|
|
2465
2747
|
|
|
2466
2748
|
// src/brain/schedule.ts
|
|
2467
|
-
function
|
|
2749
|
+
function pad23(n) {
|
|
2468
2750
|
return n < 10 ? `0${n}` : `${n}`;
|
|
2469
2751
|
}
|
|
2470
2752
|
function formatDateKey(d) {
|
|
2471
|
-
return `${d.getFullYear()}-${
|
|
2753
|
+
return `${d.getFullYear()}-${pad23(d.getMonth() + 1)}-${pad23(d.getDate())}`;
|
|
2472
2754
|
}
|
|
2473
2755
|
function formatMonthKey(d) {
|
|
2474
|
-
return `${d.getFullYear()}-${
|
|
2756
|
+
return `${d.getFullYear()}-${pad23(d.getMonth() + 1)}`;
|
|
2475
2757
|
}
|
|
2476
2758
|
|
|
2477
2759
|
// src/brain/memory.ts
|
|
@@ -2569,12 +2851,12 @@ class Brain {
|
|
|
2569
2851
|
log7.debug(`Brain constructed: id=${brainbase.brainId} name=${brainbase.displayName} space=${space.name}`);
|
|
2570
2852
|
}
|
|
2571
2853
|
async createDailySchedule(datetime) {
|
|
2572
|
-
const
|
|
2573
|
-
log7.debug(`createDailySchedule: starting for ${
|
|
2854
|
+
const dateKey2 = formatDateKey(datetime);
|
|
2855
|
+
log7.debug(`createDailySchedule: starting for ${dateKey2}`);
|
|
2574
2856
|
try {
|
|
2575
|
-
const existing = await this.memory.get(`daily-schedule:${
|
|
2857
|
+
const existing = await this.memory.get(`daily-schedule:${dateKey2}`);
|
|
2576
2858
|
if (existing) {
|
|
2577
|
-
log7.debug(`createDailySchedule: cache hit for ${
|
|
2859
|
+
log7.debug(`createDailySchedule: cache hit for ${dateKey2}`);
|
|
2578
2860
|
try {
|
|
2579
2861
|
return JSON.parse(existing.content);
|
|
2580
2862
|
} catch (parseErr) {
|
|
@@ -2602,7 +2884,7 @@ class Brain {
|
|
|
2602
2884
|
}
|
|
2603
2885
|
const instruction = await loadPrompt("DAILY_SCHEDULE");
|
|
2604
2886
|
const promptMessage = [
|
|
2605
|
-
`Target date: ${
|
|
2887
|
+
`Target date: ${dateKey2} (${datetime.toLocaleDateString("en-US", { weekday: "long" })})`,
|
|
2606
2888
|
`Personality: ${this.brainbase.baseSystemPrompt}`,
|
|
2607
2889
|
monthlySummary ? `Monthly summary for this day: ${monthlySummary}` : "(no monthly summary available for this date)",
|
|
2608
2890
|
`Recent schedule (${twoDaysAgoKey}, 2 days ago): ${twoDaysAgoSchedule ? twoDaysAgoSchedule.items.map((s) => `${s.start} ${s.activity}`).join(", ") : "(no schedule on file for 2 days ago)"}`,
|
|
@@ -2613,6 +2895,7 @@ class Brain {
|
|
|
2613
2895
|
`);
|
|
2614
2896
|
log7.debug(`createDailySchedule: calling identity model`);
|
|
2615
2897
|
const schedule = await llm.call(llm.models.identity, {
|
|
2898
|
+
caller: "daily-schedule",
|
|
2616
2899
|
instruction,
|
|
2617
2900
|
message: promptMessage,
|
|
2618
2901
|
jsonSchemaName: "daily-schedule",
|
|
@@ -2620,15 +2903,15 @@ class Brain {
|
|
|
2620
2903
|
});
|
|
2621
2904
|
log7.debug(`createDailySchedule: model returned ${schedule.items.length} slots`);
|
|
2622
2905
|
await this.memory.add({
|
|
2623
|
-
customId: `daily-schedule:${
|
|
2906
|
+
customId: `daily-schedule:${dateKey2}`,
|
|
2624
2907
|
content: JSON.stringify(schedule),
|
|
2625
2908
|
metadata: {
|
|
2626
2909
|
kind: "schedule",
|
|
2627
2910
|
source: "createDailySchedule",
|
|
2628
|
-
date:
|
|
2911
|
+
date: dateKey2
|
|
2629
2912
|
}
|
|
2630
2913
|
});
|
|
2631
|
-
log7.debug(`createDailySchedule: persisted ${
|
|
2914
|
+
log7.debug(`createDailySchedule: persisted ${dateKey2}`);
|
|
2632
2915
|
return schedule;
|
|
2633
2916
|
} catch (error) {
|
|
2634
2917
|
let reason = error instanceof Error ? error.message + `(${error.name})` : String(error);
|
|
@@ -2640,17 +2923,21 @@ class Brain {
|
|
|
2640
2923
|
}
|
|
2641
2924
|
async regenerateSchedules() {
|
|
2642
2925
|
const today = new Date;
|
|
2926
|
+
const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
|
|
2927
|
+
const monthly = await this.createMonthlySchedule(nextMonth);
|
|
2928
|
+
if (!monthly) {
|
|
2929
|
+
log7.debug(`regenerateSchedules: skip daily — monthly schedule generation failed`);
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2643
2932
|
const tomorrow = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
|
2644
2933
|
await this.createDailySchedule(tomorrow);
|
|
2645
2934
|
await this.createDailySchedule(today);
|
|
2646
|
-
const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
|
|
2647
|
-
await this.createMonthlySchedule(nextMonth);
|
|
2648
2935
|
}
|
|
2649
2936
|
async createMonthlySchedule(datetime) {
|
|
2650
2937
|
const year = datetime.getMonth() === 11 ? datetime.getFullYear() + 1 : datetime.getFullYear();
|
|
2651
2938
|
const month = (datetime.getMonth() + 1) % 12;
|
|
2652
2939
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
2653
|
-
const monthKey = `${year}-${
|
|
2940
|
+
const monthKey = `${year}-${pad23(month + 1)}`;
|
|
2654
2941
|
log7.debug(`createMonthlySchedule: starting for ${monthKey}`);
|
|
2655
2942
|
try {
|
|
2656
2943
|
const existing = await this.memory.get(`monthly-schedule:${monthKey}`);
|
|
@@ -2663,7 +2950,7 @@ class Brain {
|
|
|
2663
2950
|
}
|
|
2664
2951
|
}
|
|
2665
2952
|
const twoMonthsAgo = new Date(year, month - 2, 1);
|
|
2666
|
-
const twoMonthsAgoKey = `${twoMonthsAgo.getFullYear()}-${
|
|
2953
|
+
const twoMonthsAgoKey = `${twoMonthsAgo.getFullYear()}-${pad23(twoMonthsAgo.getMonth() + 1)}`;
|
|
2667
2954
|
log7.debug(`createMonthlySchedule: gathering context (history, ${twoMonthsAgoKey})`);
|
|
2668
2955
|
const [history, twoMonthsAgoStored] = await Promise.all([
|
|
2669
2956
|
this.getHistoryFacts(),
|
|
@@ -2691,6 +2978,7 @@ class Brain {
|
|
|
2691
2978
|
`);
|
|
2692
2979
|
log7.debug(`createMonthlySchedule: calling identity model`);
|
|
2693
2980
|
const schedule = await llm.call(llm.models.identity, {
|
|
2981
|
+
caller: "monthly-schedule",
|
|
2694
2982
|
instruction,
|
|
2695
2983
|
message: promptMessage,
|
|
2696
2984
|
jsonSchemaName: "monthly-schedule",
|
|
@@ -2721,11 +3009,11 @@ class Brain {
|
|
|
2721
3009
|
}
|
|
2722
3010
|
log7.debug(`sleepMemory: starting, ${history.length} messages`);
|
|
2723
3011
|
try {
|
|
2724
|
-
const
|
|
3012
|
+
const dateKey2 = formatDateKey(datetime);
|
|
2725
3013
|
const instruction = await loadPrompt("MEMOIR");
|
|
2726
3014
|
const historyBlock = translateMessageHistory(this.brainbase.displayName, history);
|
|
2727
3015
|
const promptMessage = [
|
|
2728
|
-
`Date: ${
|
|
3016
|
+
`Date: ${dateKey2}`,
|
|
2729
3017
|
`Personality: ${this.brainbase.baseSystemPrompt}`,
|
|
2730
3018
|
`Conversation log:`,
|
|
2731
3019
|
historyBlock
|
|
@@ -2734,20 +3022,21 @@ class Brain {
|
|
|
2734
3022
|
`);
|
|
2735
3023
|
log7.debug(`sleepMemory: calling identity model`);
|
|
2736
3024
|
const memoir = await llm.call(llm.models.identity, {
|
|
3025
|
+
caller: "sleep-memory",
|
|
2737
3026
|
instruction,
|
|
2738
3027
|
message: promptMessage
|
|
2739
3028
|
});
|
|
2740
3029
|
log7.debug(`sleepMemory: model returned ${memoir.length} chars`);
|
|
2741
3030
|
await this.memory.add({
|
|
2742
|
-
customId: `daily-journal:${
|
|
3031
|
+
customId: `daily-journal:${dateKey2}`,
|
|
2743
3032
|
content: memoir,
|
|
2744
3033
|
metadata: {
|
|
2745
3034
|
kind: "daily-journal",
|
|
2746
3035
|
source: "sleepMemory",
|
|
2747
|
-
date:
|
|
3036
|
+
date: dateKey2
|
|
2748
3037
|
}
|
|
2749
3038
|
});
|
|
2750
|
-
log7.debug(`sleepMemory: journal persisted for ${
|
|
3039
|
+
log7.debug(`sleepMemory: journal persisted for ${dateKey2}`);
|
|
2751
3040
|
return memoir;
|
|
2752
3041
|
} catch (error) {
|
|
2753
3042
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -2756,22 +3045,22 @@ class Brain {
|
|
|
2756
3045
|
}
|
|
2757
3046
|
}
|
|
2758
3047
|
async getTodayScheduledAvailability(datetime) {
|
|
2759
|
-
const
|
|
3048
|
+
const dateKey2 = formatDateKey(datetime);
|
|
2760
3049
|
try {
|
|
2761
|
-
const cached = this.availabilityCache.get(
|
|
3050
|
+
const cached = this.availabilityCache.get(dateKey2);
|
|
2762
3051
|
if (cached) {
|
|
2763
|
-
log7.debug(`getTodayScheduledAvailability: cache hit for ${
|
|
3052
|
+
log7.debug(`getTodayScheduledAvailability: cache hit for ${dateKey2}`);
|
|
2764
3053
|
return cached;
|
|
2765
3054
|
}
|
|
2766
|
-
const stored = await this.memory.get(`daily-schedule:${
|
|
3055
|
+
const stored = await this.memory.get(`daily-schedule:${dateKey2}`);
|
|
2767
3056
|
if (!stored) {
|
|
2768
|
-
log7.debug(`getTodayScheduledAvailability: no schedule for ${
|
|
3057
|
+
log7.debug(`getTodayScheduledAvailability: no schedule for ${dateKey2}`);
|
|
2769
3058
|
return null;
|
|
2770
3059
|
}
|
|
2771
3060
|
const dailySchedule = JSON.parse(stored.content);
|
|
2772
3061
|
const availability = await this.deriveAvailabilityFromSchedule(dailySchedule);
|
|
2773
|
-
this.availabilityCache.set(
|
|
2774
|
-
log7.debug(`getTodayScheduledAvailability: cached ${availability.items.length} windows for ${
|
|
3062
|
+
this.availabilityCache.set(dateKey2, availability);
|
|
3063
|
+
log7.debug(`getTodayScheduledAvailability: cached ${availability.items.length} windows for ${dateKey2}`);
|
|
2775
3064
|
return availability;
|
|
2776
3065
|
} catch (error) {
|
|
2777
3066
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -2782,7 +3071,7 @@ class Brain {
|
|
|
2782
3071
|
async getAvailability(datetime = new Date) {
|
|
2783
3072
|
const h = datetime.getHours();
|
|
2784
3073
|
const m = datetime.getMinutes();
|
|
2785
|
-
const hhmm = `${
|
|
3074
|
+
const hhmm = `${pad23(h)}:${pad23(m)}`;
|
|
2786
3075
|
const current = h * 60 + m;
|
|
2787
3076
|
const toMinutes = (s) => {
|
|
2788
3077
|
const [hh = 0, mm = 0] = s.split(":").map((x) => parseInt(x, 10));
|
|
@@ -2795,10 +3084,10 @@ class Brain {
|
|
|
2795
3084
|
return result;
|
|
2796
3085
|
}
|
|
2797
3086
|
async getCurrentAndAdjacentSlots(now) {
|
|
2798
|
-
const
|
|
2799
|
-
const stored = await this.memory.get(`daily-schedule:${
|
|
3087
|
+
const dateKey2 = formatDateKey(now);
|
|
3088
|
+
const stored = await this.memory.get(`daily-schedule:${dateKey2}`);
|
|
2800
3089
|
if (!stored) {
|
|
2801
|
-
log7.debug(`getCurrentAndAdjacentSlots: no schedule for ${
|
|
3090
|
+
log7.debug(`getCurrentAndAdjacentSlots: no schedule for ${dateKey2}`);
|
|
2802
3091
|
return [];
|
|
2803
3092
|
}
|
|
2804
3093
|
let schedule;
|
|
@@ -2831,6 +3120,7 @@ class Brain {
|
|
|
2831
3120
|
personality: this.brainbase.baseSystemPrompt
|
|
2832
3121
|
});
|
|
2833
3122
|
const result = await llm.call(llm.models.identity, {
|
|
3123
|
+
caller: "availability",
|
|
2834
3124
|
instruction,
|
|
2835
3125
|
message: promptMessage,
|
|
2836
3126
|
jsonSchemaName: "availability",
|
|
@@ -2901,74 +3191,56 @@ class Brain {
|
|
|
2901
3191
|
content: userPrompt
|
|
2902
3192
|
}
|
|
2903
3193
|
];
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
choice = await llm.chatWithTools(llm.models.conversation, {
|
|
2909
|
-
instruction: `${this.brainbase.baseSystemPrompt}
|
|
3194
|
+
try {
|
|
3195
|
+
await llm.chatWithToolExecution(llm.models.conversation, {
|
|
3196
|
+
caller: initiate ? "start-conversation" : "send-message",
|
|
3197
|
+
instruction: `${this.brainbase.baseSystemPrompt}
|
|
2910
3198
|
|
|
2911
3199
|
${instruction}`,
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
}
|
|
2928
|
-
messages.push(stripAssistantForHistory(assistantMessage));
|
|
2929
|
-
for (const call of toolCalls) {
|
|
2930
|
-
if (call.function.name === "addReplyMessage") {
|
|
2931
|
-
const content = parseAddReplyMessageArguments(call.function.arguments);
|
|
2932
|
-
if (content !== null) {
|
|
2933
|
-
log7.debug(`sendMessage: addReplyMessage[${replyMessages.length}] (${content.length} chars)`);
|
|
2934
|
-
send(content);
|
|
2935
|
-
replyMessages.push(content);
|
|
2936
|
-
} else {
|
|
3200
|
+
messages,
|
|
3201
|
+
tools,
|
|
3202
|
+
maxSteps,
|
|
3203
|
+
executeTool: async (call) => {
|
|
3204
|
+
if (call.function.name === "addReplyMessage") {
|
|
3205
|
+
const content = parseAddReplyMessageArguments(call.function.arguments);
|
|
3206
|
+
if (content !== null) {
|
|
3207
|
+
log7.debug(`sendMessage: addReplyMessage[${replyMessages.length}] (${content.length} chars)`);
|
|
3208
|
+
await send(content);
|
|
3209
|
+
replyMessages.push(content);
|
|
3210
|
+
return JSON.stringify({
|
|
3211
|
+
ok: true,
|
|
3212
|
+
index: replyMessages.length - 1
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
2937
3215
|
log7.debug(`sendMessage: addReplyMessage rejected (invalid arguments: ${call.function.arguments})`);
|
|
3216
|
+
return JSON.stringify({ ok: false, error: "invalid arguments" });
|
|
2938
3217
|
}
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
log7.debug(`sendMessage:
|
|
2948
|
-
|
|
2949
|
-
messages.push({
|
|
2950
|
-
role: "tool",
|
|
2951
|
-
toolCallId: call.id,
|
|
2952
|
-
content: result
|
|
2953
|
-
});
|
|
2954
|
-
continue;
|
|
2955
|
-
}
|
|
2956
|
-
log7.debug(`sendMessage: unknown tool "${call.function.name}"`);
|
|
2957
|
-
messages.push({
|
|
2958
|
-
role: "tool",
|
|
2959
|
-
toolCallId: call.id,
|
|
2960
|
-
content: JSON.stringify({
|
|
3218
|
+
if (call.function.name === "searchMemory") {
|
|
3219
|
+
log7.debug(`sendMessage: searchMemory tool call`);
|
|
3220
|
+
return this.executeSearchTool(call.function.arguments);
|
|
3221
|
+
}
|
|
3222
|
+
if (call.function.name === "stop") {
|
|
3223
|
+
log7.debug(`sendMessage: stop tool call`);
|
|
3224
|
+
return JSON.stringify({ ok: true });
|
|
3225
|
+
}
|
|
3226
|
+
log7.debug(`sendMessage: unknown tool "${call.function.name}"`);
|
|
3227
|
+
return JSON.stringify({
|
|
2961
3228
|
ok: false,
|
|
2962
3229
|
error: `Unknown tool: ${call.function.name}`
|
|
2963
|
-
})
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
3230
|
+
});
|
|
3231
|
+
},
|
|
3232
|
+
shouldEnd: (toolCalls) => toolCalls.some((c) => c.function.name === "stop"),
|
|
3233
|
+
onNoToolCalls: () => {
|
|
3234
|
+
if (replyMessages.length > 0)
|
|
3235
|
+
return null;
|
|
3236
|
+
return "If you do not want to send a message, call the `stop` tool explicitly to end your turn. " + "If you meant to send a message but ended by mistake, call `addReplyMessage`.";
|
|
3237
|
+
}
|
|
3238
|
+
});
|
|
3239
|
+
} catch (error) {
|
|
3240
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3241
|
+
logger.error(`sendMessage: LLM call failed: ${reason}`);
|
|
2970
3242
|
}
|
|
2971
|
-
|
|
3243
|
+
log7.debug(`sendMessage: done with ${replyMessages.length} replies`);
|
|
2972
3244
|
return replyMessages;
|
|
2973
3245
|
}
|
|
2974
3246
|
async buildMemoryBlock() {
|
|
@@ -2977,11 +3249,11 @@ ${instruction}`,
|
|
|
2977
3249
|
${facts || "(none indexed)"}`;
|
|
2978
3250
|
}
|
|
2979
3251
|
async buildScheduleBlock(now) {
|
|
2980
|
-
const
|
|
3252
|
+
const dateKey2 = formatDateKey(now);
|
|
2981
3253
|
const currentSlots = await this.getCurrentAndAdjacentSlots(now);
|
|
2982
3254
|
const currentBlock = currentSlots.length > 0 ? `Currently (around ${now.toTimeString().slice(0, 5)}):
|
|
2983
3255
|
${currentSlots.map((s) => ` ${s.start}-${s.end} ${s.activity}${s.notes ? ` (${s.notes})` : ""}`).join(`
|
|
2984
|
-
`)}` : `Currently (${
|
|
3256
|
+
`)}` : `Currently (${dateKey2} ${now.toTimeString().slice(0, 5)}): (no matching slot in today's schedule)`;
|
|
2985
3257
|
const days = [
|
|
2986
3258
|
{
|
|
2987
3259
|
label: "Yesterday",
|
|
@@ -3004,9 +3276,9 @@ ${currentBlock}
|
|
|
3004
3276
|
${blocks.join(`
|
|
3005
3277
|
`)}`;
|
|
3006
3278
|
}
|
|
3007
|
-
async getDailyScheduleSummary(
|
|
3279
|
+
async getDailyScheduleSummary(dateKey2) {
|
|
3008
3280
|
try {
|
|
3009
|
-
const stored = await this.memory.get(`daily-schedule:${
|
|
3281
|
+
const stored = await this.memory.get(`daily-schedule:${dateKey2}`);
|
|
3010
3282
|
if (!stored)
|
|
3011
3283
|
return null;
|
|
3012
3284
|
const schedule = JSON.parse(stored.content);
|
|
@@ -3077,6 +3349,7 @@ ${blocks.join(`
|
|
|
3077
3349
|
const personaInitInstruction = await loadPrompt("PERSONA_INIT");
|
|
3078
3350
|
log7.debug(`Brain.create: generating description`);
|
|
3079
3351
|
const description = await llm.call(llm.models.identity, {
|
|
3352
|
+
caller: "persona-init",
|
|
3080
3353
|
instruction: personaInitInstruction,
|
|
3081
3354
|
message: seed
|
|
3082
3355
|
});
|
|
@@ -3084,6 +3357,7 @@ ${blocks.join(`
|
|
|
3084
3357
|
const personaSystemInstruction = await loadPrompt("PERSONA_BASE_SYSTEM_PROMPT");
|
|
3085
3358
|
log7.debug(`Brain.create: generating base system prompt + dials`);
|
|
3086
3359
|
const generated = await llm.call(llm.models.identity, {
|
|
3360
|
+
caller: "base-system-prompt",
|
|
3087
3361
|
instruction: personaSystemInstruction,
|
|
3088
3362
|
message: description,
|
|
3089
3363
|
jsonSchemaName: "base-system-prompt",
|
|
@@ -3169,7 +3443,7 @@ function buildSendMessageTools() {
|
|
|
3169
3443
|
return [
|
|
3170
3444
|
{
|
|
3171
3445
|
name: "addReplyMessage",
|
|
3172
|
-
description: "Append one chat bubble to the reply stream. Call once per bubble you want to send.
|
|
3446
|
+
description: "Append one chat bubble to the reply stream. Call once per bubble you want to send. After at least one successful call, you may end your turn without calling stop.",
|
|
3173
3447
|
parameters: {
|
|
3174
3448
|
type: "object",
|
|
3175
3449
|
additionalProperties: false,
|
|
@@ -3193,6 +3467,15 @@ function buildSendMessageTools() {
|
|
|
3193
3467
|
},
|
|
3194
3468
|
required: ["query"]
|
|
3195
3469
|
}
|
|
3470
|
+
},
|
|
3471
|
+
{
|
|
3472
|
+
name: "stop",
|
|
3473
|
+
description: "End your turn without sending any further messages. Required when you choose not to send a message. Not needed once you have already called addReplyMessage at least once.",
|
|
3474
|
+
parameters: {
|
|
3475
|
+
type: "object",
|
|
3476
|
+
additionalProperties: false,
|
|
3477
|
+
properties: {}
|
|
3478
|
+
}
|
|
3196
3479
|
}
|
|
3197
3480
|
];
|
|
3198
3481
|
}
|
|
@@ -3218,13 +3501,6 @@ function parseSearchArguments(json) {
|
|
|
3218
3501
|
}
|
|
3219
3502
|
return null;
|
|
3220
3503
|
}
|
|
3221
|
-
function stripAssistantForHistory(message) {
|
|
3222
|
-
return {
|
|
3223
|
-
role: "assistant",
|
|
3224
|
-
content: message.content,
|
|
3225
|
-
toolCalls: message.toolCalls
|
|
3226
|
-
};
|
|
3227
|
-
}
|
|
3228
3504
|
|
|
3229
3505
|
// src/channel/discord.ts
|
|
3230
3506
|
import {
|
|
@@ -3244,10 +3520,18 @@ var SLEEP_MEMORY_CRON_KEY = "__sleep-memory__";
|
|
|
3244
3520
|
var SLEEP_MEMORY_CRON_PATTERN = "0 * * * *";
|
|
3245
3521
|
var START_CONVERSATION_CRON_KEY = "__start-conversation__";
|
|
3246
3522
|
var START_CONVERSATION_CRON_PATTERN = "*/10 * * * *";
|
|
3247
|
-
var
|
|
3248
|
-
var
|
|
3249
|
-
var
|
|
3250
|
-
var
|
|
3523
|
+
var SCHEDULE_CRON_KEY = "__schedule__";
|
|
3524
|
+
var SCHEDULE_CRON_PATTERN = "0 0 * * *";
|
|
3525
|
+
var SCHEDULE_NOON_CRON_KEY = "__schedule-noon__";
|
|
3526
|
+
var SCHEDULE_NOON_CRON_PATTERN = "0 12 * * *";
|
|
3527
|
+
var DO_ACTIONS = ["generateSchedule", "sleepMemory"];
|
|
3528
|
+
var VIEW_THINGS = [
|
|
3529
|
+
"daily-schedule",
|
|
3530
|
+
"monthly-schedule",
|
|
3531
|
+
"sending-queue",
|
|
3532
|
+
"deferred-queue",
|
|
3533
|
+
"today-availability"
|
|
3534
|
+
];
|
|
3251
3535
|
|
|
3252
3536
|
class BaseChannel {
|
|
3253
3537
|
brain;
|
|
@@ -3267,24 +3551,27 @@ class BaseChannel {
|
|
|
3267
3551
|
static activeChannels = new Map;
|
|
3268
3552
|
constructor(brain) {
|
|
3269
3553
|
this.brain = brain;
|
|
3270
|
-
this.registerCron(SLEEP_MEMORY_CRON_KEY, SLEEP_MEMORY_CRON_PATTERN,
|
|
3271
|
-
|
|
3554
|
+
this.registerCron(SLEEP_MEMORY_CRON_KEY, SLEEP_MEMORY_CRON_PATTERN, () => this.runSleepMemory());
|
|
3555
|
+
this.registerCron(SCHEDULE_CRON_KEY, SCHEDULE_CRON_PATTERN, () => this.regenerateSchedules());
|
|
3556
|
+
this.registerCron(SCHEDULE_NOON_CRON_KEY, SCHEDULE_NOON_CRON_PATTERN, () => this.regenerateSchedules());
|
|
3557
|
+
this.registerCron(START_CONVERSATION_CRON_KEY, START_CONVERSATION_CRON_PATTERN, () => this.runStartConversation());
|
|
3558
|
+
}
|
|
3559
|
+
async runSleepMemory(force = false) {
|
|
3560
|
+
const dateKey2 = formatDateKey(new Date);
|
|
3561
|
+
if (!force) {
|
|
3272
3562
|
const availability = await this.brain.getAvailability();
|
|
3273
3563
|
if (availability.status !== "offline") {
|
|
3274
3564
|
logger.debug(`sleepMemory cron: skip — availability=${availability.status}`);
|
|
3275
3565
|
return;
|
|
3276
3566
|
}
|
|
3277
|
-
const existing = await this.brain.memory.get(`daily-journal:${
|
|
3567
|
+
const existing = await this.brain.memory.get(`daily-journal:${dateKey2}`);
|
|
3278
3568
|
if (existing) {
|
|
3279
|
-
logger.debug(`sleepMemory cron: skip — journal for ${
|
|
3569
|
+
logger.debug(`sleepMemory cron: skip — journal for ${dateKey2} exists`);
|
|
3280
3570
|
return;
|
|
3281
3571
|
}
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
this.registerCron(DAILY_SCHEDULE_CRON_KEY, DAILY_SCHEDULE_CRON_PATTERN, () => this.regenerateSchedules());
|
|
3286
|
-
this.registerCron(DAILY_SCHEDULE_NOON_CRON_KEY, DAILY_SCHEDULE_NOON_CRON_PATTERN, () => this.regenerateSchedules());
|
|
3287
|
-
this.registerCron(START_CONVERSATION_CRON_KEY, START_CONVERSATION_CRON_PATTERN, () => this.runStartConversation());
|
|
3572
|
+
}
|
|
3573
|
+
const history = await this.getMessageHistoryBetween(new Date(Date.now() - 24 * 60 * 60 * 1000), new Date);
|
|
3574
|
+
await this.brain.sleepMemory(new Date, history);
|
|
3288
3575
|
}
|
|
3289
3576
|
async regenerateSchedules() {
|
|
3290
3577
|
logger.debug(`regenerateSchedules: tick for ${this.brain.brainbase.displayName}`);
|
|
@@ -3305,8 +3592,8 @@ class BaseChannel {
|
|
|
3305
3592
|
return;
|
|
3306
3593
|
}
|
|
3307
3594
|
const now = new Date;
|
|
3308
|
-
const
|
|
3309
|
-
const count = this.startConversationCounters.get(
|
|
3595
|
+
const dateKey2 = formatDateKey(now);
|
|
3596
|
+
const count = this.startConversationCounters.get(dateKey2) ?? 0;
|
|
3310
3597
|
const countThreshold = this.brain.brainbase.startConversationCountThreshold;
|
|
3311
3598
|
if (count >= countThreshold)
|
|
3312
3599
|
return;
|
|
@@ -3319,7 +3606,7 @@ class BaseChannel {
|
|
|
3319
3606
|
});
|
|
3320
3607
|
if (replies.length === 0)
|
|
3321
3608
|
return;
|
|
3322
|
-
this.startConversationCounters.set(
|
|
3609
|
+
this.startConversationCounters.set(dateKey2, count + 1);
|
|
3323
3610
|
this.startConversationTimeout = true;
|
|
3324
3611
|
setTimeout(() => {
|
|
3325
3612
|
this.startConversationTimeout = false;
|
|
@@ -3506,6 +3793,88 @@ class BaseChannel {
|
|
|
3506
3793
|
static all() {
|
|
3507
3794
|
return Array.from(BaseChannel.activeChannels.values());
|
|
3508
3795
|
}
|
|
3796
|
+
static forceDo(brainId, action) {
|
|
3797
|
+
const channel = BaseChannel.activeChannels.get(brainId);
|
|
3798
|
+
if (!channel) {
|
|
3799
|
+
return {
|
|
3800
|
+
ok: false,
|
|
3801
|
+
error: `no active channel for brain "${brainId}" (is it activated and daemon running?)`
|
|
3802
|
+
};
|
|
3803
|
+
}
|
|
3804
|
+
const displayName = channel.brain.brainbase.displayName;
|
|
3805
|
+
logger.info(`do ${action}: queued for "${displayName}" (${brainId})`);
|
|
3806
|
+
(async () => {
|
|
3807
|
+
try {
|
|
3808
|
+
if (action === "generateSchedule") {
|
|
3809
|
+
await channel.regenerateSchedules();
|
|
3810
|
+
} else {
|
|
3811
|
+
await channel.runSleepMemory(true);
|
|
3812
|
+
}
|
|
3813
|
+
logger.success(`do ${action}: done for "${displayName}" (${brainId})`);
|
|
3814
|
+
} catch (error) {
|
|
3815
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3816
|
+
logger.error(`do ${action}: failed for "${displayName}" (${brainId}): ${reason}`);
|
|
3817
|
+
}
|
|
3818
|
+
})();
|
|
3819
|
+
return { ok: true, displayName };
|
|
3820
|
+
}
|
|
3821
|
+
static async view(brainId, thing) {
|
|
3822
|
+
const channel = BaseChannel.activeChannels.get(brainId);
|
|
3823
|
+
if (!channel) {
|
|
3824
|
+
return {
|
|
3825
|
+
ok: false,
|
|
3826
|
+
error: `no active channel for brain "${brainId}" (is it activated and daemon running?)`
|
|
3827
|
+
};
|
|
3828
|
+
}
|
|
3829
|
+
const displayName = channel.brain.brainbase.displayName;
|
|
3830
|
+
logger.debug(`view ${thing}: "${displayName}" (${brainId})`);
|
|
3831
|
+
return {
|
|
3832
|
+
ok: true,
|
|
3833
|
+
displayName,
|
|
3834
|
+
value: await channel.readView(thing)
|
|
3835
|
+
};
|
|
3836
|
+
}
|
|
3837
|
+
async readView(thing) {
|
|
3838
|
+
const now = new Date;
|
|
3839
|
+
switch (thing) {
|
|
3840
|
+
case "daily-schedule": {
|
|
3841
|
+
const key = formatDateKey(now);
|
|
3842
|
+
const stored = await this.brain.memory.get(`daily-schedule:${key}`);
|
|
3843
|
+
if (!stored)
|
|
3844
|
+
return null;
|
|
3845
|
+
try {
|
|
3846
|
+
return { key, schedule: JSON.parse(stored.content) };
|
|
3847
|
+
} catch {
|
|
3848
|
+
return { key, raw: stored.content };
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
case "monthly-schedule": {
|
|
3852
|
+
const key = formatMonthKey(now);
|
|
3853
|
+
const stored = await this.brain.memory.get(`monthly-schedule:${key}`);
|
|
3854
|
+
if (!stored)
|
|
3855
|
+
return null;
|
|
3856
|
+
try {
|
|
3857
|
+
return { key, schedule: JSON.parse(stored.content) };
|
|
3858
|
+
} catch {
|
|
3859
|
+
return { key, raw: stored.content };
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
case "sending-queue":
|
|
3863
|
+
return this.isSendingQueue.map((m) => ({
|
|
3864
|
+
sender: m.sender,
|
|
3865
|
+
time: m.time.toISOString(),
|
|
3866
|
+
content: m.content
|
|
3867
|
+
}));
|
|
3868
|
+
case "deferred-queue":
|
|
3869
|
+
return this.deferredQueue.map((m) => ({
|
|
3870
|
+
sender: m.sender,
|
|
3871
|
+
time: m.time.toISOString(),
|
|
3872
|
+
content: m.content
|
|
3873
|
+
}));
|
|
3874
|
+
case "today-availability":
|
|
3875
|
+
return await this.brain.getTodayScheduledAvailability(now);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3509
3878
|
static async shutdownAll() {
|
|
3510
3879
|
await Promise.all(BaseChannel.all().map((c) => c.shutdown()));
|
|
3511
3880
|
}
|
|
@@ -3891,8 +4260,8 @@ class TelegramChannel extends BaseChannel {
|
|
|
3891
4260
|
|
|
3892
4261
|
// src/utils/daemonClient.ts
|
|
3893
4262
|
import { connect } from "node:net";
|
|
3894
|
-
import { join as
|
|
3895
|
-
var DAEMON_SOCKET_PATH =
|
|
4263
|
+
import { join as join4 } from "node:path";
|
|
4264
|
+
var DAEMON_SOCKET_PATH = join4(config.brainboxRoot, "daemon.sock");
|
|
3896
4265
|
async function sendToDaemon(payload) {
|
|
3897
4266
|
logger.debug(`sendToDaemon: command="${payload.command}" args=${JSON.stringify(payload.args) ?? "null"}`);
|
|
3898
4267
|
let reply;
|
|
@@ -3954,7 +4323,7 @@ function exchangeOnce(payload) {
|
|
|
3954
4323
|
// src/commands/daemon.ts
|
|
3955
4324
|
import { createServer } from "node:net";
|
|
3956
4325
|
import { chmodSync, unlinkSync } from "node:fs";
|
|
3957
|
-
import { join as
|
|
4326
|
+
import { join as join5 } from "node:path";
|
|
3958
4327
|
|
|
3959
4328
|
// src/commands/daemon/commands.ts
|
|
3960
4329
|
var log8 = logger.child("daemon-cmd");
|
|
@@ -4015,6 +4384,63 @@ defineCommand({
|
|
|
4015
4384
|
}
|
|
4016
4385
|
});
|
|
4017
4386
|
|
|
4387
|
+
// src/commands/daemon/doCommand.ts
|
|
4388
|
+
defineCommand({
|
|
4389
|
+
name: "do",
|
|
4390
|
+
handler: async (args) => {
|
|
4391
|
+
const action = args?.action;
|
|
4392
|
+
const brainId = args?.brainId;
|
|
4393
|
+
logger.debug(`do handler: action="${action}" brainId="${brainId}"`);
|
|
4394
|
+
if (typeof action !== "string" || !DO_ACTIONS.includes(action)) {
|
|
4395
|
+
return {
|
|
4396
|
+
ok: false,
|
|
4397
|
+
error: `invalid action (expected one of: ${DO_ACTIONS.join(", ")})`
|
|
4398
|
+
};
|
|
4399
|
+
}
|
|
4400
|
+
if (typeof brainId !== "string" || brainId.trim().length === 0) {
|
|
4401
|
+
return { ok: false, error: "missing brainId" };
|
|
4402
|
+
}
|
|
4403
|
+
const result = BaseChannel.forceDo(brainId.trim(), action);
|
|
4404
|
+
if (!result.ok)
|
|
4405
|
+
return result;
|
|
4406
|
+
return {
|
|
4407
|
+
ok: true,
|
|
4408
|
+
result: { action, brainId: brainId.trim(), displayName: result.displayName }
|
|
4409
|
+
};
|
|
4410
|
+
}
|
|
4411
|
+
});
|
|
4412
|
+
|
|
4413
|
+
// src/commands/daemon/viewCommand.ts
|
|
4414
|
+
defineCommand({
|
|
4415
|
+
name: "view",
|
|
4416
|
+
handler: async (args) => {
|
|
4417
|
+
const thing = args?.thing;
|
|
4418
|
+
const brainId = args?.brainId;
|
|
4419
|
+
logger.debug(`view handler: thing="${thing}" brainId="${brainId}"`);
|
|
4420
|
+
if (typeof thing !== "string" || !VIEW_THINGS.includes(thing)) {
|
|
4421
|
+
return {
|
|
4422
|
+
ok: false,
|
|
4423
|
+
error: `invalid thing (expected one of: ${VIEW_THINGS.join(", ")})`
|
|
4424
|
+
};
|
|
4425
|
+
}
|
|
4426
|
+
if (typeof brainId !== "string" || brainId.trim().length === 0) {
|
|
4427
|
+
return { ok: false, error: "missing brainId" };
|
|
4428
|
+
}
|
|
4429
|
+
const result = await BaseChannel.view(brainId.trim(), thing);
|
|
4430
|
+
if (!result.ok)
|
|
4431
|
+
return result;
|
|
4432
|
+
return {
|
|
4433
|
+
ok: true,
|
|
4434
|
+
result: {
|
|
4435
|
+
thing,
|
|
4436
|
+
brainId: brainId.trim(),
|
|
4437
|
+
displayName: result.displayName,
|
|
4438
|
+
value: result.value
|
|
4439
|
+
}
|
|
4440
|
+
};
|
|
4441
|
+
}
|
|
4442
|
+
});
|
|
4443
|
+
|
|
4018
4444
|
// src/commands/daemon.ts
|
|
4019
4445
|
async function startChannels() {
|
|
4020
4446
|
const items = await brainManager.listAvailableBrain();
|
|
@@ -4049,9 +4475,10 @@ async function startChannels() {
|
|
|
4049
4475
|
return started;
|
|
4050
4476
|
}
|
|
4051
4477
|
async function daemon() {
|
|
4052
|
-
const
|
|
4053
|
-
logger.configure({
|
|
4054
|
-
|
|
4478
|
+
const logDir = join5(config.brainboxRoot, "logs");
|
|
4479
|
+
logger.configure({ logDir });
|
|
4480
|
+
configureLlmLog(config.debug ? join5(logDir, "llm") : undefined);
|
|
4481
|
+
logger.debug(`daemon: boot (debug=${config.debug}, logDir=${logDir}, llmLog=${config.debug})`);
|
|
4055
4482
|
const started = await startChannels();
|
|
4056
4483
|
if (started === 0) {
|
|
4057
4484
|
logger.info("No activated brains with channels. Daemon idling.");
|
|
@@ -4218,6 +4645,33 @@ async function removeBrain(brainId) {
|
|
|
4218
4645
|
}
|
|
4219
4646
|
logger.success(`Removed brain "${brain.displayName}" (${chalk2.cyan(brainId)})`);
|
|
4220
4647
|
}
|
|
4648
|
+
async function doAction(action, brainId) {
|
|
4649
|
+
if (!DO_ACTIONS.includes(action)) {
|
|
4650
|
+
logger.error(`Unknown action "${action}". Expected one of: ${DO_ACTIONS.join(", ")}`);
|
|
4651
|
+
process.exit(1);
|
|
4652
|
+
}
|
|
4653
|
+
logger.debug(`do: action=${action} brainId=${brainId}`);
|
|
4654
|
+
const response = await sendToDaemon({
|
|
4655
|
+
command: "do",
|
|
4656
|
+
args: { action, brainId }
|
|
4657
|
+
});
|
|
4658
|
+
const name = response.result?.displayName ?? brainId;
|
|
4659
|
+
logger.success(`Successfully sent ${action} for "${name}" (${brainId}).`);
|
|
4660
|
+
}
|
|
4661
|
+
async function viewThing(thing, brainId) {
|
|
4662
|
+
if (!VIEW_THINGS.includes(thing)) {
|
|
4663
|
+
logger.error(`Unknown thing "${thing}". Expected one of: ${VIEW_THINGS.join(", ")}`);
|
|
4664
|
+
process.exit(1);
|
|
4665
|
+
}
|
|
4666
|
+
logger.debug(`view: thing=${thing} brainId=${brainId}`);
|
|
4667
|
+
const response = await sendToDaemon({
|
|
4668
|
+
command: "view",
|
|
4669
|
+
args: { thing, brainId }
|
|
4670
|
+
});
|
|
4671
|
+
const name = response.result?.displayName ?? brainId;
|
|
4672
|
+
logger.info(`${thing} — "${name}" (${brainId})`);
|
|
4673
|
+
console.log(JSON.stringify(response.result?.value ?? null, null, 2));
|
|
4674
|
+
}
|
|
4221
4675
|
function register3(program) {
|
|
4222
4676
|
const cmd = registerCommand(program, {
|
|
4223
4677
|
name: "brain",
|
|
@@ -4228,6 +4682,8 @@ function register3(program) {
|
|
|
4228
4682
|
cmd.command("remove <brainId>").description("Remove a brain and its memory").action(removeBrain);
|
|
4229
4683
|
cmd.command("activate <brainId>").description("Activate a brain").action(activateBrain);
|
|
4230
4684
|
cmd.command("deactivate <brainId>").description("Deactivate a brain").action(deactivateBrain);
|
|
4685
|
+
cmd.command("do <action> <brainId>").description(`Force-run a daemon job (${DO_ACTIONS.join(" | ")}) for a live brain`).action(doAction);
|
|
4686
|
+
cmd.command("view <thing> <brainId>").description(`Inspect a live brain value (${VIEW_THINGS.join(" | ")})`).action(viewThing);
|
|
4231
4687
|
return cmd;
|
|
4232
4688
|
}
|
|
4233
4689
|
|
|
@@ -5572,12 +6028,12 @@ function register8(program) {
|
|
|
5572
6028
|
|
|
5573
6029
|
// src/index.ts
|
|
5574
6030
|
var __filename3 = fileURLToPath2(import.meta.url);
|
|
5575
|
-
var __dirname3 =
|
|
6031
|
+
var __dirname3 = dirname3(__filename3);
|
|
5576
6032
|
logger.configure({ level: config.debug ? "debug" : "info" });
|
|
5577
6033
|
logger.debug(`brainbox starting (debug=${config.debug}, root=${config.brainboxRoot})`);
|
|
5578
6034
|
function getVersion() {
|
|
5579
6035
|
try {
|
|
5580
|
-
const pkgPath =
|
|
6036
|
+
const pkgPath = join6(__dirname3, "..", "package.json");
|
|
5581
6037
|
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
5582
6038
|
return pkg.version ?? "0.0.0";
|
|
5583
6039
|
} catch {
|