@p-sw/brainbox 0.1.2-alpha.10 → 0.1.2-alpha.11

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.
Files changed (2) hide show
  1. package/dist/index.js +157 -218
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -550,7 +550,7 @@ class LLMExecutor {
550
550
  });
551
551
  };
552
552
  if (conv.provider === id.provider) {
553
- return withLlmLogging(build(conv.provider));
553
+ return build(conv.provider);
554
554
  }
555
555
  const modelToProvider = {
556
556
  [conv.model]: conv.provider,
@@ -561,7 +561,7 @@ class LLMExecutor {
561
561
  [id.provider]: build(id.provider)
562
562
  };
563
563
  const fallback = instances[conv.provider];
564
- return withLlmLogging(new class extends LLMExecutor {
564
+ return new class extends LLMExecutor {
565
565
  providerName = "dispatch";
566
566
  models = {
567
567
  conversation: conv.model,
@@ -575,126 +575,21 @@ class LLMExecutor {
575
575
  const exec = instances[modelToProvider[model] ?? ""] ?? fallback;
576
576
  return exec.chatWithTools(model, options);
577
577
  }
578
- });
579
- }
580
- }
581
- function formatPayload(value) {
582
- if (typeof value === "string")
583
- return value;
584
- try {
585
- return JSON.stringify(value, null, 2);
586
- } catch {
587
- return String(value);
578
+ };
588
579
  }
589
580
  }
590
- function resolveCaller(options, fallback) {
581
+ function resolveLlmCaller(options, fallback = "llm") {
591
582
  return options.caller ?? options.jsonSchemaName ?? fallback;
592
583
  }
593
- function withLlmLogging(inner) {
594
- return new class extends LLMExecutor {
595
- providerName = inner.providerName;
596
- models = inner.models;
597
- async call(model, options) {
598
- const jsonMode = "jsonSchemaName" in options;
599
- const caller = resolveCaller({
600
- caller: options.caller,
601
- jsonSchemaName: jsonMode ? options.jsonSchemaName : undefined
602
- }, "call");
603
- const requestBody = [
604
- `provider: ${inner.providerName}`,
605
- `model: ${model}`,
606
- `caller: ${caller}`,
607
- `reasoningEffort: ${options.reasoningEffort ?? "-"}`,
608
- `jsonSchemaName: ${jsonMode ? options.jsonSchemaName : "-"}`,
609
- "",
610
- "--- instruction ---",
611
- options.instruction,
612
- "",
613
- "--- message ---",
614
- options.message,
615
- ...jsonMode ? ["", "--- jsonSchema ---", formatPayload(options.jsonSchema)] : []
616
- ].join(`
617
- `);
618
- try {
619
- const result = await inner.call(model, options);
620
- if (isLlmLogEnabled()) {
621
- writeLlmExchange(caller, [
622
- "======== REQUEST ========",
623
- requestBody,
624
- "",
625
- "======== RESPONSE ========",
626
- formatPayload(result),
627
- ""
628
- ].join(`
629
- `));
630
- }
631
- return result;
632
- } catch (err) {
633
- const reason = err instanceof Error ? err.message : String(err);
634
- if (isLlmLogEnabled()) {
635
- writeLlmExchange(caller, [
636
- "======== REQUEST ========",
637
- requestBody,
638
- "",
639
- "======== ERROR ========",
640
- reason,
641
- ""
642
- ].join(`
643
- `));
644
- }
645
- throw err;
646
- }
647
- }
648
- async chatWithTools(model, options) {
649
- const caller = resolveCaller(options, "chat");
650
- const requestBody = [
651
- `provider: ${inner.providerName}`,
652
- `model: ${model}`,
653
- `caller: ${caller}`,
654
- `reasoningEffort: ${options.reasoningEffort ?? "-"}`,
655
- `parallelToolCalls: ${options.parallelToolCalls ?? false}`,
656
- "",
657
- "--- instruction ---",
658
- options.instruction,
659
- "",
660
- "--- messages ---",
661
- formatPayload(options.messages),
662
- "",
663
- "--- tools ---",
664
- formatPayload(options.tools)
665
- ].join(`
584
+ function logLlmWire(caller, request, response) {
585
+ if (!isLlmLogEnabled())
586
+ return;
587
+ writeLlmExchange(caller, `======== REQUEST ========
588
+ ${request}
589
+
590
+ ======== RESPONSE ========
591
+ ${response}
666
592
  `);
667
- try {
668
- const result = await inner.chatWithTools(model, options);
669
- if (isLlmLogEnabled()) {
670
- writeLlmExchange(caller, [
671
- "======== REQUEST ========",
672
- requestBody,
673
- "",
674
- "======== RESPONSE ========",
675
- formatPayload(result),
676
- ""
677
- ].join(`
678
- `));
679
- }
680
- return result;
681
- } catch (err) {
682
- const reason = err instanceof Error ? err.message : String(err);
683
- if (isLlmLogEnabled()) {
684
- writeLlmExchange(caller, [
685
- "======== REQUEST ========",
686
- requestBody,
687
- "",
688
- "======== ERROR ========",
689
- reason,
690
- ""
691
- ].join(`
692
- `));
693
- }
694
- throw err;
695
- }
696
- }
697
- };
698
593
  }
699
594
  function listProviderNames() {
700
595
  return LLMExecutor.listProviderNames();
@@ -764,27 +659,27 @@ class OpenRouterExecutor extends LLMExecutor {
764
659
  async call(model, options) {
765
660
  const jsonMode = "jsonSchemaName" in options;
766
661
  log3.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
767
- const result = await this.client.chat.send({
768
- chatRequest: {
769
- model,
770
- messages: [
771
- { role: "system", content: options.instruction },
772
- { role: "user", content: options.message }
773
- ],
774
- reasoning: {
775
- effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
776
- },
777
- responseFormat: jsonMode ? {
778
- type: "json_schema",
779
- jsonSchema: {
780
- name: options.jsonSchemaName,
781
- schema: options.jsonSchema,
782
- strict: true
783
- }
784
- } : { type: "text" },
785
- stream: false
786
- }
787
- });
662
+ const chatRequest = {
663
+ model,
664
+ messages: [
665
+ { role: "system", content: options.instruction },
666
+ { role: "user", content: options.message }
667
+ ],
668
+ reasoning: {
669
+ effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
670
+ },
671
+ responseFormat: jsonMode ? {
672
+ type: "json_schema",
673
+ jsonSchema: {
674
+ name: options.jsonSchemaName,
675
+ schema: options.jsonSchema,
676
+ strict: true
677
+ }
678
+ } : { type: "text" },
679
+ stream: false
680
+ };
681
+ const result = await this.client.chat.send({ chatRequest });
682
+ logLlmWire(resolveLlmCaller(options), JSON.stringify(chatRequest), JSON.stringify(result));
788
683
  const raw = result.choices[0]?.message?.content;
789
684
  const content = typeof raw === "string" ? stripThinkTags(raw) : raw;
790
685
  if (!content) {
@@ -796,22 +691,22 @@ class OpenRouterExecutor extends LLMExecutor {
796
691
  }
797
692
  async chatWithTools(model, options) {
798
693
  log3.debug(`chatWithTools: model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
799
- const result = await this.client.chat.send({
800
- chatRequest: {
801
- model,
802
- messages: [
803
- { role: "system", content: options.instruction },
804
- ...options.messages.map(toOrMessage)
805
- ],
806
- reasoning: {
807
- effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
808
- },
809
- responseFormat: { type: "text" },
810
- tools: options.tools.map(toOrTool),
811
- parallelToolCalls: options.parallelToolCalls ?? false,
812
- stream: false
813
- }
814
- });
694
+ const chatRequest = {
695
+ model,
696
+ messages: [
697
+ { role: "system", content: options.instruction },
698
+ ...options.messages.map(toOrMessage)
699
+ ],
700
+ reasoning: {
701
+ effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
702
+ },
703
+ responseFormat: { type: "text" },
704
+ tools: options.tools.map(toOrTool),
705
+ parallelToolCalls: options.parallelToolCalls ?? false,
706
+ stream: false
707
+ };
708
+ const result = await this.client.chat.send({ chatRequest });
709
+ logLlmWire(resolveLlmCaller(options), JSON.stringify(chatRequest), JSON.stringify(result));
815
710
  const choice = result.choices[0];
816
711
  if (!choice) {
817
712
  log3.debug(`chatWithTools: no choice in response`);
@@ -925,7 +820,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
925
820
  }
926
821
  return url.toString();
927
822
  }
928
- async sendRequest(body, reasoningEffort) {
823
+ async sendRequest(body, reasoningEffort, caller) {
929
824
  const modelName = body["model"];
930
825
  const modelStr = typeof modelName === "string" ? modelName : "";
931
826
  const url = this.buildRequestUrl(modelStr, reasoningEffort);
@@ -935,21 +830,34 @@ class OpenAICompatibleExecutor extends LLMExecutor {
935
830
  ...authHeader,
936
831
  ...this.defaultHeaders
937
832
  };
833
+ const requestRaw = JSON.stringify(body);
938
834
  const res = await fetch(url, {
939
835
  method: "POST",
940
836
  headers,
941
- body: JSON.stringify(body)
837
+ body: requestRaw
942
838
  });
839
+ const responseRaw = await res.text().catch(() => "");
840
+ logLlmWire(caller, requestRaw, responseRaw);
943
841
  if (!res.ok) {
944
- const text = await res.text().catch(() => "");
945
- log4.error(`${this.providerName}: HTTP ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
842
+ log4.error(`${this.providerName}: HTTP ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
946
843
  throw new Error(`${this.providerName} request failed: ${res.status} ${res.statusText}`);
947
844
  }
948
- const data = await res.json();
845
+ let data;
846
+ try {
847
+ data = JSON.parse(responseRaw);
848
+ } catch {
849
+ throw new Error(`${this.providerName}: invalid JSON response`);
850
+ }
949
851
  if (data.error) {
950
852
  log4.error(`${this.providerName}: API error ${data.error.type ?? ""} ${data.error.message ?? ""}`);
951
853
  throw new Error(`${this.providerName} API error: ${data.error.message ?? "unknown"}`);
952
854
  }
855
+ const baseCode = data.base_resp?.status_code;
856
+ if (typeof baseCode === "number" && baseCode !== 0) {
857
+ const msg = data.base_resp?.status_msg?.trim() || `status_code ${baseCode}`;
858
+ log4.error(`${this.providerName}: base_resp ${baseCode} ${msg}`);
859
+ throw new Error(`${this.providerName} API error: ${msg}`);
860
+ }
953
861
  return data;
954
862
  }
955
863
  async call(model, options) {
@@ -965,12 +873,15 @@ class OpenAICompatibleExecutor extends LLMExecutor {
965
873
  responseFormat: jsonMode ? buildResponseFormat(options.jsonSchemaName, options.jsonSchema) : undefined,
966
874
  reasoningEffort: reasoning
967
875
  });
968
- const data = await this.sendRequest(body, options.reasoningEffort);
969
- const raw = data.choices?.[0]?.message?.content;
876
+ const data = await this.sendRequest(body, options.reasoningEffort, resolveLlmCaller(options));
877
+ const choice = data.choices?.[0];
878
+ const raw = choice?.message?.content;
970
879
  const content = typeof raw === "string" ? stripThinkTags(raw) : raw;
971
880
  if (!content) {
972
- log4.debug(`call: empty content in choice 0`);
973
- throw new Error("Empty response from model");
881
+ const finish = choice?.finish_reason ?? "no-choice";
882
+ const reasoningLen = typeof choice?.message?.reasoning_content === "string" ? choice.message.reasoning_content.length : 0;
883
+ log4.debug(`call: empty content in choice 0 finish_reason=${finish} reasoning_len=${reasoningLen} rawType=${raw === null ? "null" : typeof raw}`);
884
+ throw new Error(reasoningLen > 0 ? `Empty response from model (finish_reason=${finish}; reasoning present but no content)` : "Empty response from model");
974
885
  }
975
886
  log4.debug(`call: response ${content.length} chars`);
976
887
  return jsonMode ? parseModelJson(content) : content;
@@ -988,7 +899,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
988
899
  parallelToolCalls: options.parallelToolCalls ?? false,
989
900
  reasoningEffort: reasoning
990
901
  });
991
- const data = await this.sendRequest(body, options.reasoningEffort);
902
+ const data = await this.sendRequest(body, options.reasoningEffort, resolveLlmCaller(options));
992
903
  const choice = data.choices?.[0];
993
904
  if (!choice) {
994
905
  log4.debug(`chatWithTools: no choice in response`);
@@ -1290,10 +1201,15 @@ class ZenMuxExecutor extends OpenAICompatibleExecutor {
1290
1201
  // src/provider/providers/minimax.ts
1291
1202
  class MiniMaxBase extends OpenAICompatibleExecutor {
1292
1203
  buildBody(opts) {
1204
+ const jsonMode = opts.responseFormat !== undefined;
1293
1205
  const body = super.buildBody(opts);
1294
1206
  delete body["reasoning_effort"];
1207
+ delete body["response_format"];
1208
+ delete body["parallel_tool_calls"];
1295
1209
  body["reasoning_split"] = true;
1296
- body["thinking"] = opts.reasoningEffort && opts.reasoningEffort !== "none" ? { type: "adaptive" } : { type: "disabled" };
1210
+ body["max_completion_tokens"] = 16384;
1211
+ const wantThink = !jsonMode && opts.reasoningEffort !== undefined && opts.reasoningEffort !== "none";
1212
+ body["thinking"] = wantThink ? { type: "adaptive" } : { type: "disabled" };
1297
1213
  return body;
1298
1214
  }
1299
1215
  }
@@ -1501,21 +1417,23 @@ class CloudflareWorkersExecutor extends LLMExecutor {
1501
1417
  const accountId = readAuthString(opts.auth, "accountId", "CLOUDFLARE_ACCOUNT_ID");
1502
1418
  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";
1503
1419
  }
1504
- async run(model, body) {
1420
+ async run(model, body, caller) {
1505
1421
  const url = `${this.baseURL}/${encodeURIComponent(model)}`;
1422
+ const requestRaw = JSON.stringify(body);
1506
1423
  const res = await fetch(url, {
1507
1424
  method: "POST",
1508
1425
  headers: {
1509
1426
  Authorization: `Bearer ${this.apiKey}`,
1510
1427
  "Content-Type": "application/json"
1511
1428
  },
1512
- body: JSON.stringify(body)
1429
+ body: requestRaw
1513
1430
  });
1431
+ const responseRaw = await res.text().catch(() => "");
1432
+ logLlmWire(caller, requestRaw, responseRaw);
1514
1433
  if (!res.ok) {
1515
- const text = await res.text().catch(() => "");
1516
- throw new Error(`cloudflare-workers request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
1434
+ throw new Error(`cloudflare-workers request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
1517
1435
  }
1518
- return await res.json();
1436
+ return JSON.parse(responseRaw);
1519
1437
  }
1520
1438
  async call(model, options) {
1521
1439
  const jsonMode = "jsonSchemaName" in options;
@@ -1536,7 +1454,7 @@ class CloudflareWorkersExecutor extends LLMExecutor {
1536
1454
  if (reasoning !== "none") {
1537
1455
  body["reasoning_effort"] = reasoning;
1538
1456
  }
1539
- const data = await this.run(model, body);
1457
+ const data = await this.run(model, body, resolveLlmCaller(options));
1540
1458
  if (data.errors && data.errors.length > 0) {
1541
1459
  throw new Error(`cloudflare-workers API error: ${data.errors.map((e) => e.message).join("; ")}`);
1542
1460
  }
@@ -1565,7 +1483,7 @@ class CloudflareWorkersExecutor extends LLMExecutor {
1565
1483
  if (reasoning !== "none") {
1566
1484
  body["reasoning_effort"] = reasoning;
1567
1485
  }
1568
- const data = await this.run(model, body);
1486
+ const data = await this.run(model, body, resolveLlmCaller(options));
1569
1487
  if (data.errors && data.errors.length > 0) {
1570
1488
  throw new Error(`cloudflare-workers API error: ${data.errors.map((e) => e.message).join("; ")}`);
1571
1489
  }
@@ -1732,7 +1650,8 @@ class AnthropicExecutor extends LLMExecutor {
1732
1650
  this.baseURL = (extra.baseURL ?? readAuthString(opts.auth, "baseURL", "ANTHROPIC_BASE_URL") ?? "https://api.anthropic.com").replace(/\/v1\/?$/, "");
1733
1651
  this.apiVersion = extra.apiVersion ?? "2023-06-01";
1734
1652
  }
1735
- async send(body) {
1653
+ async send(body, caller) {
1654
+ const requestRaw = JSON.stringify(body);
1736
1655
  const res = await fetch(`${this.baseURL}/v1/messages`, {
1737
1656
  method: "POST",
1738
1657
  headers: {
@@ -1740,13 +1659,14 @@ class AnthropicExecutor extends LLMExecutor {
1740
1659
  "anthropic-version": this.apiVersion,
1741
1660
  "Content-Type": "application/json"
1742
1661
  },
1743
- body: JSON.stringify(body)
1662
+ body: requestRaw
1744
1663
  });
1664
+ const responseRaw = await res.text().catch(() => "");
1665
+ logLlmWire(caller, requestRaw, responseRaw);
1745
1666
  if (!res.ok) {
1746
- const text = await res.text().catch(() => "");
1747
- throw new Error(`anthropic request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
1667
+ throw new Error(`anthropic request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
1748
1668
  }
1749
- return await res.json();
1669
+ return JSON.parse(responseRaw);
1750
1670
  }
1751
1671
  async call(model, options) {
1752
1672
  const jsonMode = "jsonSchemaName" in options;
@@ -1768,7 +1688,7 @@ class AnthropicExecutor extends LLMExecutor {
1768
1688
  budget_tokens: budget
1769
1689
  };
1770
1690
  }
1771
- const data = await this.send(body);
1691
+ const data = await this.send(body, resolveLlmCaller(options));
1772
1692
  if (data.error) {
1773
1693
  throw new Error(`anthropic API error: ${data.error.message ?? "unknown"}`);
1774
1694
  }
@@ -1799,7 +1719,7 @@ class AnthropicExecutor extends LLMExecutor {
1799
1719
  budget_tokens: budget
1800
1720
  };
1801
1721
  }
1802
- const data = await this.send(body);
1722
+ const data = await this.send(body, resolveLlmCaller(options));
1803
1723
  if (data.error) {
1804
1724
  throw new Error(`anthropic API error: ${data.error.message ?? "unknown"}`);
1805
1725
  }
@@ -1962,7 +1882,7 @@ class BedrockExecutor extends LLMExecutor {
1962
1882
  this.sessionToken = readAuthString(opts.auth, "sessionToken", "AWS_SESSION_TOKEN");
1963
1883
  this.baseURL = `https://bedrock-runtime.${this.region}.amazonaws.com`;
1964
1884
  }
1965
- async invoke(model, body) {
1885
+ async invoke(model, body, caller) {
1966
1886
  const bodyStr = JSON.stringify(body);
1967
1887
  const path = `/model/${encodeURIComponent(model)}/invoke`;
1968
1888
  const headers = signRequest({
@@ -1981,11 +1901,12 @@ class BedrockExecutor extends LLMExecutor {
1981
1901
  headers,
1982
1902
  body: bodyStr
1983
1903
  });
1904
+ const responseRaw = await res.text().catch(() => "");
1905
+ logLlmWire(caller, bodyStr, responseRaw);
1984
1906
  if (!res.ok) {
1985
- const text = await res.text().catch(() => "");
1986
- throw new Error(`bedrock request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
1907
+ throw new Error(`bedrock request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
1987
1908
  }
1988
- return await res.json();
1909
+ return JSON.parse(responseRaw);
1989
1910
  }
1990
1911
  async call(model, options) {
1991
1912
  const jsonMode = "jsonSchemaName" in options;
@@ -2000,7 +1921,7 @@ class BedrockExecutor extends LLMExecutor {
2000
1921
  system: options.instruction,
2001
1922
  messages: [{ role: "user", content: options.message }]
2002
1923
  };
2003
- const data = await this.invoke(model, body);
1924
+ const data = await this.invoke(model, body, resolveLlmCaller(options));
2004
1925
  const { text } = extractAnthropicContent(data);
2005
1926
  if (!text) {
2006
1927
  throw new Error("Empty response from model");
@@ -2021,7 +1942,7 @@ class BedrockExecutor extends LLMExecutor {
2021
1942
  messages: msgs,
2022
1943
  tools: options.tools.map(toAnthropicTool2)
2023
1944
  };
2024
- const data = await this.invoke(model, body);
1945
+ const data = await this.invoke(model, body, resolveLlmCaller(options));
2025
1946
  const { text, toolCalls } = extractAnthropicContent(data);
2026
1947
  return { message: { content: text || undefined, toolCalls } };
2027
1948
  }
@@ -2137,21 +2058,23 @@ class VertexExecutor extends LLMExecutor {
2137
2058
  this.region = extra.region ?? readAuthString(opts.auth, "region", "GOOGLE_CLOUD_REGION") ?? "us-central1";
2138
2059
  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";
2139
2060
  }
2140
- async generate(model, body) {
2061
+ async generate(model, body, caller) {
2141
2062
  const url = `${this.baseURL}/${encodeURIComponent(model)}:generateContent`;
2063
+ const requestRaw = JSON.stringify(body);
2142
2064
  const res = await fetch(url, {
2143
2065
  method: "POST",
2144
2066
  headers: {
2145
2067
  Authorization: `Bearer ${this.apiKey}`,
2146
2068
  "Content-Type": "application/json"
2147
2069
  },
2148
- body: JSON.stringify(body)
2070
+ body: requestRaw
2149
2071
  });
2072
+ const responseRaw = await res.text().catch(() => "");
2073
+ logLlmWire(caller, requestRaw, responseRaw);
2150
2074
  if (!res.ok) {
2151
- const text = await res.text().catch(() => "");
2152
- throw new Error(`vertex request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
2075
+ throw new Error(`vertex request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
2153
2076
  }
2154
- return await res.json();
2077
+ return JSON.parse(responseRaw);
2155
2078
  }
2156
2079
  async call(model, options) {
2157
2080
  const jsonMode = "jsonSchemaName" in options;
@@ -2172,7 +2095,7 @@ class VertexExecutor extends LLMExecutor {
2172
2095
  responseSchema: options.jsonSchema
2173
2096
  };
2174
2097
  }
2175
- const data = await this.generate(model, body);
2098
+ const data = await this.generate(model, body, resolveLlmCaller(options));
2176
2099
  if (data.error) {
2177
2100
  throw new Error(`vertex API error: ${data.error.message ?? "unknown"}`);
2178
2101
  }
@@ -2191,7 +2114,7 @@ class VertexExecutor extends LLMExecutor {
2191
2114
  systemInstruction: { parts: [{ text: options.instruction }] },
2192
2115
  tools: options.tools.length > 0 ? [toGeminiTools(options.tools)] : undefined
2193
2116
  };
2194
- const data = await this.generate(model, body);
2117
+ const data = await this.generate(model, body, resolveLlmCaller(options));
2195
2118
  if (data.error) {
2196
2119
  throw new Error(`vertex API error: ${data.error.message ?? "unknown"}`);
2197
2120
  }
@@ -2267,20 +2190,22 @@ class GitLabDuoExecutor extends LLMExecutor {
2267
2190
  const extra = parsed.success ? parsed.data : {};
2268
2191
  this.baseURL = (extra.baseURL ?? readAuthString(opts.auth, "baseURL", "GITLAB_BASE_URL") ?? "https://gitlab.com/api/v4/ai/llm/proxy").replace(/\/+$/, "");
2269
2192
  }
2270
- async send(body) {
2193
+ async send(body, caller) {
2194
+ const requestRaw = JSON.stringify(body);
2271
2195
  const res = await fetch(this.baseURL, {
2272
2196
  method: "POST",
2273
2197
  headers: {
2274
2198
  Authorization: `Bearer ${this.apiKey}`,
2275
2199
  "Content-Type": "application/json"
2276
2200
  },
2277
- body: JSON.stringify(body)
2201
+ body: requestRaw
2278
2202
  });
2203
+ const responseRaw = await res.text().catch(() => "");
2204
+ logLlmWire(caller, requestRaw, responseRaw);
2279
2205
  if (!res.ok) {
2280
- const text = await res.text().catch(() => "");
2281
- throw new Error(`gitlab-duo request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
2206
+ throw new Error(`gitlab-duo request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
2282
2207
  }
2283
- return await res.json();
2208
+ return JSON.parse(responseRaw);
2284
2209
  }
2285
2210
  async call(model, options) {
2286
2211
  const jsonMode = "jsonSchemaName" in options;
@@ -2293,7 +2218,7 @@ class GitLabDuoExecutor extends LLMExecutor {
2293
2218
  { role: "user", content: options.message }
2294
2219
  ]
2295
2220
  };
2296
- const data = await this.send(body);
2221
+ const data = await this.send(body, resolveLlmCaller(options));
2297
2222
  if (data.error) {
2298
2223
  throw new Error(`gitlab-duo API error: ${data.error.message ?? "unknown"}`);
2299
2224
  }
@@ -2314,7 +2239,7 @@ class GitLabDuoExecutor extends LLMExecutor {
2314
2239
  ],
2315
2240
  tools: options.tools.map(toTool)
2316
2241
  };
2317
- const data = await this.send(body);
2242
+ const data = await this.send(body, resolveLlmCaller(options));
2318
2243
  if (data.error) {
2319
2244
  throw new Error(`gitlab-duo API error: ${data.error.message ?? "unknown"}`);
2320
2245
  }
@@ -2382,20 +2307,22 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2382
2307
  const account = extra.account ?? readAuthString(opts.auth, "account", "SNOWFLAKE_ACCOUNT") ?? "";
2383
2308
  this.baseURL = account ? `https://${account}.snowflakecomputing.com/api/v2/cortex/inference:complete` : "https://__account__.snowflakecomputing.com/api/v2/cortex/inference:complete";
2384
2309
  }
2385
- async send(body) {
2310
+ async send(body, caller) {
2311
+ const requestRaw = JSON.stringify(body);
2386
2312
  const res = await fetch(this.baseURL, {
2387
2313
  method: "POST",
2388
2314
  headers: {
2389
2315
  Authorization: `Bearer ${this.apiKey}`,
2390
2316
  "Content-Type": "application/json"
2391
2317
  },
2392
- body: JSON.stringify(body)
2318
+ body: requestRaw
2393
2319
  });
2320
+ const responseRaw = await res.text().catch(() => "");
2321
+ logLlmWire(caller, requestRaw, responseRaw);
2394
2322
  if (!res.ok) {
2395
- const text = await res.text().catch(() => "");
2396
- throw new Error(`snowflake-cortex request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
2323
+ throw new Error(`snowflake-cortex request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
2397
2324
  }
2398
- return await res.json();
2325
+ return JSON.parse(responseRaw);
2399
2326
  }
2400
2327
  async call(model, options) {
2401
2328
  const jsonMode = "jsonSchemaName" in options;
@@ -2412,7 +2339,7 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2412
2339
  if (reasoning !== "none") {
2413
2340
  body["reasoning_effort"] = reasoning;
2414
2341
  }
2415
- const data = await this.send(body);
2342
+ const data = await this.send(body, resolveLlmCaller(options));
2416
2343
  if (data.error) {
2417
2344
  throw new Error(`snowflake-cortex API error: ${data.error.message ?? "unknown"}`);
2418
2345
  }
@@ -2433,7 +2360,7 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2433
2360
  ],
2434
2361
  tools: options.tools.map(toCortexTool)
2435
2362
  };
2436
- const data = await this.send(body);
2363
+ const data = await this.send(body, resolveLlmCaller(options));
2437
2364
  if (data.error) {
2438
2365
  throw new Error(`snowflake-cortex API error: ${data.error.message ?? "unknown"}`);
2439
2366
  }
@@ -2822,7 +2749,11 @@ class Brain {
2822
2749
  async regenerateSchedules() {
2823
2750
  const today = new Date;
2824
2751
  const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
2825
- await this.createMonthlySchedule(nextMonth);
2752
+ const monthly = await this.createMonthlySchedule(nextMonth);
2753
+ if (!monthly) {
2754
+ log7.debug(`regenerateSchedules: skip daily — monthly schedule generation failed`);
2755
+ return;
2756
+ }
2826
2757
  const tomorrow = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
2827
2758
  await this.createDailySchedule(tomorrow);
2828
2759
  await this.createDailySchedule(today);
@@ -3704,7 +3635,7 @@ class BaseChannel {
3704
3635
  static all() {
3705
3636
  return Array.from(BaseChannel.activeChannels.values());
3706
3637
  }
3707
- static async forceDo(brainId, action) {
3638
+ static forceDo(brainId, action) {
3708
3639
  const channel = BaseChannel.activeChannels.get(brainId);
3709
3640
  if (!channel) {
3710
3641
  return {
@@ -3713,12 +3644,20 @@ class BaseChannel {
3713
3644
  };
3714
3645
  }
3715
3646
  const displayName = channel.brain.brainbase.displayName;
3716
- logger.info(`do ${action}: forcing for "${displayName}" (${brainId})`);
3717
- if (action === "generateSchedule") {
3718
- await channel.regenerateSchedules();
3719
- } else {
3720
- await channel.runSleepMemory(true);
3721
- }
3647
+ logger.info(`do ${action}: queued for "${displayName}" (${brainId})`);
3648
+ (async () => {
3649
+ try {
3650
+ if (action === "generateSchedule") {
3651
+ await channel.regenerateSchedules();
3652
+ } else {
3653
+ await channel.runSleepMemory(true);
3654
+ }
3655
+ logger.success(`do ${action}: done for "${displayName}" (${brainId})`);
3656
+ } catch (error) {
3657
+ const reason = error instanceof Error ? error.message : String(error);
3658
+ logger.error(`do ${action}: failed for "${displayName}" (${brainId}): ${reason}`);
3659
+ }
3660
+ })();
3722
3661
  return { ok: true, displayName };
3723
3662
  }
3724
3663
  static async view(brainId, thing) {
@@ -4303,7 +4242,7 @@ defineCommand({
4303
4242
  if (typeof brainId !== "string" || brainId.trim().length === 0) {
4304
4243
  return { ok: false, error: "missing brainId" };
4305
4244
  }
4306
- const result = await BaseChannel.forceDo(brainId.trim(), action);
4245
+ const result = BaseChannel.forceDo(brainId.trim(), action);
4307
4246
  if (!result.ok)
4308
4247
  return result;
4309
4248
  return {
@@ -4380,8 +4319,8 @@ async function startChannels() {
4380
4319
  async function daemon() {
4381
4320
  const logDir = join5(config.brainboxRoot, "logs");
4382
4321
  logger.configure({ logDir });
4383
- configureLlmLog(join5(logDir, "llm"));
4384
- logger.debug(`daemon: boot (logDir=${logDir}, llmLogDir=${join5(logDir, "llm")})`);
4322
+ configureLlmLog(config.debug ? join5(logDir, "llm") : undefined);
4323
+ logger.debug(`daemon: boot (debug=${config.debug}, logDir=${logDir}, llmLog=${config.debug})`);
4385
4324
  const started = await startChannels();
4386
4325
  if (started === 0) {
4387
4326
  logger.info("No activated brains with channels. Daemon idling.");
@@ -4559,7 +4498,7 @@ async function doAction(action, brainId) {
4559
4498
  args: { action, brainId }
4560
4499
  });
4561
4500
  const name = response.result?.displayName ?? brainId;
4562
- logger.success(`Forced ${action} for "${name}" (${brainId}).`);
4501
+ logger.success(`Successfully sent ${action} for "${name}" (${brainId}).`);
4563
4502
  }
4564
4503
  async function viewThing(thing, brainId) {
4565
4504
  if (!VIEW_THINGS.includes(thing)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p-sw/brainbox",
3
- "version": "0.1.2-alpha.10",
3
+ "version": "0.1.2-alpha.11",
4
4
  "module": "dist/index.js",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",