@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 dirname4, join as join5 } from "path";
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 { existsSync, mkdirSync, createWriteStream } from "fs";
12
- import { dirname } from "path";
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
- function openFile(path) {
39
- if (path === shared.file)
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 ("file" in options)
64
- openFile(options.file);
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
- const pad = (n) => n.toString().padStart(2, "0");
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 (shared.fileStream) {
118
- shared.fileStream.write(shared.json ? jsonLine : fileLine);
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
- shared.fileStream?.end();
150
- shared.fileStream = undefined;
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 dirname2, join, resolve } from "path";
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"]) : join(homedir(), ".brainbox");
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 = () => join(brainboxRoot, file);
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(dirname2(p), { recursive: true });
177
- writeFileSync(p, templateStr);
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(dirname2(p), { recursive: true });
195
- writeFileSync(p, stringifyYaml(value));
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 join2 } from "path";
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 join2(this.root, "brains.json");
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
- try {
431
- return JSON.parse(cleaned);
432
- } catch (first) {
433
- const fence = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/i);
434
- if (fence?.[1]) {
435
- return JSON.parse(fence[1].trim());
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
- throw first;
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 result = await this.client.chat.send({
588
- chatRequest: {
589
- model,
590
- messages: [
591
- { role: "system", content: options.instruction },
592
- { role: "user", content: options.message }
593
- ],
594
- reasoning: {
595
- effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
596
- },
597
- responseFormat: jsonMode ? {
598
- type: "json_schema",
599
- jsonSchema: {
600
- name: options.jsonSchemaName,
601
- schema: options.jsonSchema,
602
- strict: true
603
- }
604
- } : { type: "text" },
605
- stream: false
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 result = await this.client.chat.send({
620
- chatRequest: {
621
- model,
622
- messages: [
623
- { role: "system", content: options.instruction },
624
- ...options.messages.map(toOrMessage)
625
- ],
626
- reasoning: {
627
- effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
628
- },
629
- responseFormat: { type: "text" },
630
- tools: options.tools.map(toOrTool),
631
- parallelToolCalls: options.parallelToolCalls ?? false,
632
- stream: false
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: JSON.stringify(body)
974
+ body: requestRaw
762
975
  });
976
+ const responseRaw = await res.text().catch(() => "");
977
+ logLlmWire(caller, requestRaw, responseRaw);
763
978
  if (!res.ok) {
764
- const text = await res.text().catch(() => "");
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
- const data = await res.json();
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 raw = data.choices?.[0]?.message?.content;
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
- log4.debug(`call: empty content in choice 0`);
793
- throw new Error("Empty response from model");
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["thinking"] = opts.reasoningEffort && opts.reasoningEffort !== "none" ? { type: "adaptive" } : { type: "disabled" };
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: JSON.stringify(body)
1586
+ body: requestRaw
1333
1587
  });
1588
+ const responseRaw = await res.text().catch(() => "");
1589
+ logLlmWire(caller, requestRaw, responseRaw);
1334
1590
  if (!res.ok) {
1335
- const text = await res.text().catch(() => "");
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 await res.json();
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 system;
1479
- const msgs = [];
1733
+ let system2;
1734
+ const msgs2 = [];
1480
1735
  for (const m of messages) {
1481
1736
  if (m.role === "system") {
1482
- system = (system ? system + `
1737
+ system2 = (system2 ? system2 + `
1483
1738
 
1484
1739
  ` : "") + m.content;
1485
1740
  continue;
1486
1741
  }
1487
1742
  if (m.role === "user") {
1488
- msgs.push({ role: "user", content: m.content });
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
- msgs.push({ role: "assistant", content: blocks });
1766
+ msgs2.push({ role: "assistant", content: blocks });
1512
1767
  continue;
1513
1768
  }
1514
1769
  if (m.role === "tool") {
1515
- msgs.push({
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: JSON.stringify(body)
1819
+ body: requestRaw
1564
1820
  });
1821
+ const responseRaw = await res.text().catch(() => "");
1822
+ logLlmWire(caller, requestRaw, responseRaw);
1565
1823
  if (!res.ok) {
1566
- const text = await res.text().catch(() => "");
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 await res.json();
1826
+ return JSON.parse(responseRaw);
1570
1827
  }
1571
1828
  async call(model, options) {
1572
- const jsonMode = "jsonSchemaName" in options;
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=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
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 jsonMode ? parseModelJson(text) : text;
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: system ?? options.instruction,
1611
- messages: msgs,
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 pad2 = (n) => n.toString().padStart(2, "0");
1919
+ var pad22 = (n) => n.toString().padStart(2, "0");
1642
1920
  function amzDate(now) {
1643
1921
  return {
1644
- date: `${now.getUTCFullYear()}${pad2(now.getUTCMonth() + 1)}${pad2(now.getUTCDate())}`,
1645
- datetime: `${now.getUTCFullYear()}${pad2(now.getUTCMonth() + 1)}${pad2(now.getUTCDate())}T` + `${pad2(now.getUTCHours())}${pad2(now.getUTCMinutes())}${pad2(now.getUTCSeconds())}Z`
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
- const text = await res.text().catch(() => "");
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 await res.json();
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
- log5.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
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 jsonMode ? parseModelJson(text) : text;
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
- const data = await this.invoke(model, body);
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: JSON.stringify(body)
2216
+ body: requestRaw
1969
2217
  });
2218
+ const responseRaw = await res.text().catch(() => "");
2219
+ logLlmWire(caller, requestRaw, responseRaw);
1970
2220
  if (!res.ok) {
1971
- const text = await res.text().catch(() => "");
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 await res.json();
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: JSON.stringify(body)
2347
+ body: requestRaw
2098
2348
  });
2349
+ const responseRaw = await res.text().catch(() => "");
2350
+ logLlmWire(caller, requestRaw, responseRaw);
2099
2351
  if (!res.ok) {
2100
- const text = await res.text().catch(() => "");
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 await res.json();
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
- log5.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
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 jsonMode ? parseModelJson(content) : content;
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: JSON.stringify(body)
2478
+ body: requestRaw
2213
2479
  });
2480
+ const responseRaw = await res.text().catch(() => "");
2481
+ logLlmWire(caller, requestRaw, responseRaw);
2214
2482
  if (!res.ok) {
2215
- const text = await res.text().catch(() => "");
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 await res.json();
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
- log5.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
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 jsonMode ? parseModelJson(content) : content;
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 dirname3 } from "path";
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 = dirname3(__filename2);
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 pad22(n) {
2749
+ function pad23(n) {
2468
2750
  return n < 10 ? `0${n}` : `${n}`;
2469
2751
  }
2470
2752
  function formatDateKey(d) {
2471
- return `${d.getFullYear()}-${pad22(d.getMonth() + 1)}-${pad22(d.getDate())}`;
2753
+ return `${d.getFullYear()}-${pad23(d.getMonth() + 1)}-${pad23(d.getDate())}`;
2472
2754
  }
2473
2755
  function formatMonthKey(d) {
2474
- return `${d.getFullYear()}-${pad22(d.getMonth() + 1)}`;
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 dateKey = formatDateKey(datetime);
2573
- log7.debug(`createDailySchedule: starting for ${dateKey}`);
2854
+ const dateKey2 = formatDateKey(datetime);
2855
+ log7.debug(`createDailySchedule: starting for ${dateKey2}`);
2574
2856
  try {
2575
- const existing = await this.memory.get(`daily-schedule:${dateKey}`);
2857
+ const existing = await this.memory.get(`daily-schedule:${dateKey2}`);
2576
2858
  if (existing) {
2577
- log7.debug(`createDailySchedule: cache hit for ${dateKey}`);
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: ${dateKey} (${datetime.toLocaleDateString("en-US", { weekday: "long" })})`,
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:${dateKey}`,
2906
+ customId: `daily-schedule:${dateKey2}`,
2624
2907
  content: JSON.stringify(schedule),
2625
2908
  metadata: {
2626
2909
  kind: "schedule",
2627
2910
  source: "createDailySchedule",
2628
- date: dateKey
2911
+ date: dateKey2
2629
2912
  }
2630
2913
  });
2631
- log7.debug(`createDailySchedule: persisted ${dateKey}`);
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}-${pad22(month + 1)}`;
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()}-${pad22(twoMonthsAgo.getMonth() + 1)}`;
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 dateKey = formatDateKey(datetime);
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: ${dateKey}`,
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:${dateKey}`,
3031
+ customId: `daily-journal:${dateKey2}`,
2743
3032
  content: memoir,
2744
3033
  metadata: {
2745
3034
  kind: "daily-journal",
2746
3035
  source: "sleepMemory",
2747
- date: dateKey
3036
+ date: dateKey2
2748
3037
  }
2749
3038
  });
2750
- log7.debug(`sleepMemory: journal persisted for ${dateKey}`);
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 dateKey = formatDateKey(datetime);
3048
+ const dateKey2 = formatDateKey(datetime);
2760
3049
  try {
2761
- const cached = this.availabilityCache.get(dateKey);
3050
+ const cached = this.availabilityCache.get(dateKey2);
2762
3051
  if (cached) {
2763
- log7.debug(`getTodayScheduledAvailability: cache hit for ${dateKey}`);
3052
+ log7.debug(`getTodayScheduledAvailability: cache hit for ${dateKey2}`);
2764
3053
  return cached;
2765
3054
  }
2766
- const stored = await this.memory.get(`daily-schedule:${dateKey}`);
3055
+ const stored = await this.memory.get(`daily-schedule:${dateKey2}`);
2767
3056
  if (!stored) {
2768
- log7.debug(`getTodayScheduledAvailability: no schedule for ${dateKey}`);
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(dateKey, availability);
2774
- log7.debug(`getTodayScheduledAvailability: cached ${availability.items.length} windows for ${dateKey}`);
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 = `${pad22(h)}:${pad22(m)}`;
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 dateKey = formatDateKey(now);
2799
- const stored = await this.memory.get(`daily-schedule:${dateKey}`);
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 ${dateKey}`);
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
- for (let step = 0;step < maxSteps; step += 1) {
2905
- log7.debug(`sendMessage: step ${step + 1}/${maxSteps} → model`);
2906
- let choice;
2907
- try {
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
- messages,
2913
- tools
2914
- });
2915
- } catch (error) {
2916
- const reason = error instanceof Error ? error.message : String(error);
2917
- logger.error(`sendMessage: LLM call failed at step ${step}: ${reason}`);
2918
- return replyMessages;
2919
- }
2920
- const assistantMessage = choice.message;
2921
- const toolCalls = assistantMessage.toolCalls ?? [];
2922
- const hasContent = typeof assistantMessage.content === "string" && assistantMessage.content.length > 0;
2923
- log7.debug(`sendMessage: step ${step + 1} → toolCalls=${toolCalls.length} hasContent=${hasContent}`);
2924
- if (toolCalls.length === 0) {
2925
- log7.debug(`sendMessage: model returned no tool calls; finalising with ${replyMessages.length} replies`);
2926
- return replyMessages;
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
- messages.push({
2940
- role: "tool",
2941
- toolCallId: call.id,
2942
- content: content === null ? JSON.stringify({ ok: false, error: "invalid arguments" }) : JSON.stringify({ ok: true, index: replyMessages.length - 1 })
2943
- });
2944
- continue;
2945
- }
2946
- if (call.function.name === "searchMemory") {
2947
- log7.debug(`sendMessage: searchMemory tool call`);
2948
- const result = await this.executeSearchTool(call.function.arguments);
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
- if (!hasContent && toolCalls.every((c) => c.function.name === "searchMemory")) {
2967
- log7.debug(`sendMessage: step was pure searchMemory, looping back`);
2968
- continue;
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
- logger.warn(`sendMessage: reached maxSteps (${maxSteps}) without final reply`);
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 dateKey = formatDateKey(now);
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 (${dateKey} ${now.toTimeString().slice(0, 5)}): (no matching slot in today's schedule)`;
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(dateKey) {
3279
+ async getDailyScheduleSummary(dateKey2) {
3008
3280
  try {
3009
- const stored = await this.memory.get(`daily-schedule:${dateKey}`);
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. Do not call when you are done just return text without tool calls.",
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 DAILY_SCHEDULE_CRON_KEY = "__daily-schedule__";
3248
- var DAILY_SCHEDULE_CRON_PATTERN = "0 0 * * *";
3249
- var DAILY_SCHEDULE_NOON_CRON_KEY = "__daily-schedule-noon__";
3250
- var DAILY_SCHEDULE_NOON_CRON_PATTERN = "0 12 * * *";
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, async () => {
3271
- const dateKey = formatDateKey(new Date);
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:${dateKey}`);
3567
+ const existing = await this.brain.memory.get(`daily-journal:${dateKey2}`);
3278
3568
  if (existing) {
3279
- logger.debug(`sleepMemory cron: skip — journal for ${dateKey} exists`);
3569
+ logger.debug(`sleepMemory cron: skip — journal for ${dateKey2} exists`);
3280
3570
  return;
3281
3571
  }
3282
- const history = await this.getMessageHistoryBetween(new Date(Date.now() - 24 * 60 * 60 * 1000), new Date);
3283
- await this.brain.sleepMemory(new Date, history);
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 dateKey = formatDateKey(now);
3309
- const count = this.startConversationCounters.get(dateKey) ?? 0;
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(dateKey, count + 1);
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 join3 } from "node:path";
3895
- var DAEMON_SOCKET_PATH = join3(config.brainboxRoot, "daemon.sock");
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 join4 } from "node:path";
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 logFile = join4(config.brainboxRoot, "brainbox.log");
4053
- logger.configure({ file: logFile });
4054
- logger.debug(`daemon: boot (log=${logFile})`);
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 = dirname4(__filename3);
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 = join5(__dirname3, "..", "package.json");
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 {