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

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 +281 -156
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -489,15 +489,100 @@ function stripThinkTags(content) {
489
489
  }
490
490
  function parseModelJson(content) {
491
491
  const cleaned = stripThinkTags(content);
492
- try {
493
- return JSON.parse(cleaned);
494
- } catch (first) {
495
- const fence = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/i);
496
- if (fence?.[1]) {
497
- 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;
540
+ }
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);
498
551
  }
499
- throw first;
500
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);
584
+ }
585
+ throw new Error("Empty response from model");
501
586
  }
502
587
  function readAuthString(auth, key, envName) {
503
588
  const fromAuth = typeof auth?.[key] === "string" ? auth[key] : undefined;
@@ -512,6 +597,42 @@ function readAuthString(auth, key, envName) {
512
597
  }
513
598
 
514
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
+ return last;
619
+ messages.push({
620
+ role: "assistant",
621
+ content: last.message.content,
622
+ toolCalls
623
+ });
624
+ for (const call of toolCalls) {
625
+ const content = await options.executeTool(call);
626
+ messages.push({
627
+ role: "tool",
628
+ toolCallId: call.id,
629
+ content
630
+ });
631
+ }
632
+ }
633
+ log2.warn(`chatWithToolExecution: reached maxSteps (${maxSteps}) without final reply`);
634
+ return last;
635
+ }
515
636
  static providers = [];
516
637
  static registerProvider(p) {
517
638
  LLMExecutor.providers.push(p);
@@ -781,6 +902,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
781
902
  chatPath;
782
903
  noBearerPrefix;
783
904
  reasoningEffortInQuery;
905
+ supportsResponseFormat;
784
906
  constructor(opts) {
785
907
  super();
786
908
  this.providerName = opts.providerName;
@@ -790,6 +912,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
790
912
  this.chatPath = opts.chatPath ?? "/chat/completions";
791
913
  this.noBearerPrefix = opts.noBearerPrefix ?? false;
792
914
  this.reasoningEffortInQuery = opts.reasoningEffortInQuery ?? false;
915
+ this.supportsResponseFormat = opts.supportsResponseFormat ?? true;
793
916
  this.models = {
794
917
  conversation: opts.conversationModel,
795
918
  identity: opts.identityModel
@@ -861,6 +984,9 @@ class OpenAICompatibleExecutor extends LLMExecutor {
861
984
  return data;
862
985
  }
863
986
  async call(model, options) {
987
+ if ("jsonSchemaName" in options && !this.supportsResponseFormat) {
988
+ return this.callJsonViaTool(model, options);
989
+ }
864
990
  const jsonMode = "jsonSchemaName" in options;
865
991
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
866
992
  log4.debug(`call: provider=${this.providerName} model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
@@ -886,6 +1012,19 @@ class OpenAICompatibleExecutor extends LLMExecutor {
886
1012
  log4.debug(`call: response ${content.length} chars`);
887
1013
  return jsonMode ? parseModelJson(content) : content;
888
1014
  }
1015
+ async callJsonViaTool(model, options) {
1016
+ const { toolName, tool, instruction } = buildStructuredJsonRequest(options);
1017
+ log4.debug(`callJsonViaTool: provider=${this.providerName} model=${model} tool=${toolName}`);
1018
+ const choice = await this.chatWithTools(model, {
1019
+ caller: options.caller ?? options.jsonSchemaName,
1020
+ instruction,
1021
+ messages: [{ role: "user", content: options.message }],
1022
+ tools: [tool],
1023
+ reasoningEffort: "none",
1024
+ parallelToolCalls: false
1025
+ });
1026
+ return parseStructuredJsonResult(choice, toolName);
1027
+ }
889
1028
  async chatWithTools(model, options) {
890
1029
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
891
1030
  log4.debug(`chatWithTools: provider=${this.providerName} model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
@@ -1219,7 +1358,8 @@ function minimaxOpts(providerName, baseURL, opts) {
1219
1358
  baseURL,
1220
1359
  apiKey: opts.apiKey,
1221
1360
  conversationModel: opts.conversationModel,
1222
- identityModel: opts.identityModel
1361
+ identityModel: opts.identityModel,
1362
+ supportsResponseFormat: false
1223
1363
  };
1224
1364
  }
1225
1365
 
@@ -1295,7 +1435,8 @@ class LmStudioExecutor extends OpenAICompatibleExecutor {
1295
1435
  baseURL: "http://127.0.0.1:1234/v1",
1296
1436
  apiKey: opts.apiKey || "lm-studio",
1297
1437
  conversationModel: opts.conversationModel,
1298
- identityModel: opts.identityModel
1438
+ identityModel: opts.identityModel,
1439
+ supportsResponseFormat: false
1299
1440
  });
1300
1441
  }
1301
1442
  }
@@ -1308,7 +1449,8 @@ class OllamaExecutor extends OpenAICompatibleExecutor {
1308
1449
  baseURL: "http://127.0.0.1:11434/v1",
1309
1450
  apiKey: opts.apiKey || "ollama",
1310
1451
  conversationModel: opts.conversationModel,
1311
- identityModel: opts.identityModel
1452
+ identityModel: opts.identityModel,
1453
+ supportsResponseFormat: false
1312
1454
  });
1313
1455
  }
1314
1456
  }
@@ -1334,7 +1476,8 @@ class LlamaCppExecutor extends OpenAICompatibleExecutor {
1334
1476
  baseURL: "http://127.0.0.1:8080/v1",
1335
1477
  apiKey: opts.apiKey || "llama.cpp",
1336
1478
  conversationModel: opts.conversationModel,
1337
- identityModel: opts.identityModel
1479
+ identityModel: opts.identityModel,
1480
+ supportsResponseFormat: false
1338
1481
  });
1339
1482
  }
1340
1483
  }
@@ -1573,17 +1716,17 @@ var AnthropicAuthSchema = z4.object({
1573
1716
  apiVersion: z4.string().optional()
1574
1717
  }).loose();
1575
1718
  function toAnthropicMessages(messages) {
1576
- let system;
1577
- const msgs = [];
1719
+ let system2;
1720
+ const msgs2 = [];
1578
1721
  for (const m of messages) {
1579
1722
  if (m.role === "system") {
1580
- system = (system ? system + `
1723
+ system2 = (system2 ? system2 + `
1581
1724
 
1582
1725
  ` : "") + m.content;
1583
1726
  continue;
1584
1727
  }
1585
1728
  if (m.role === "user") {
1586
- msgs.push({ role: "user", content: m.content });
1729
+ msgs2.push({ role: "user", content: m.content });
1587
1730
  continue;
1588
1731
  }
1589
1732
  if (m.role === "assistant") {
@@ -1606,11 +1749,11 @@ function toAnthropicMessages(messages) {
1606
1749
  });
1607
1750
  }
1608
1751
  }
1609
- msgs.push({ role: "assistant", content: blocks });
1752
+ msgs2.push({ role: "assistant", content: blocks });
1610
1753
  continue;
1611
1754
  }
1612
1755
  if (m.role === "tool") {
1613
- msgs.push({
1756
+ msgs2.push({
1614
1757
  role: "user",
1615
1758
  content: [
1616
1759
  {
@@ -1622,7 +1765,7 @@ function toAnthropicMessages(messages) {
1622
1765
  });
1623
1766
  }
1624
1767
  }
1625
- return { system, msgs };
1768
+ return { system: system2, msgs: msgs2 };
1626
1769
  }
1627
1770
  function toAnthropicTool(t) {
1628
1771
  return {
@@ -1669,10 +1812,25 @@ class AnthropicExecutor extends LLMExecutor {
1669
1812
  return JSON.parse(responseRaw);
1670
1813
  }
1671
1814
  async call(model, options) {
1672
- const jsonMode = "jsonSchemaName" in options;
1815
+ if ("jsonSchemaName" in options) {
1816
+ const { toolName, tool, instruction } = buildStructuredJsonRequest({
1817
+ instruction: options.instruction,
1818
+ jsonSchemaName: options.jsonSchemaName,
1819
+ jsonSchema: options.jsonSchema
1820
+ });
1821
+ const choice = await this.chatWithTools(model, {
1822
+ caller: options.caller ?? options.jsonSchemaName,
1823
+ instruction,
1824
+ messages: [{ role: "user", content: options.message }],
1825
+ tools: [tool],
1826
+ reasoningEffort: "none",
1827
+ toolChoice: { type: "tool", name: toolName }
1828
+ });
1829
+ return parseStructuredJsonResult(choice, toolName);
1830
+ }
1673
1831
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
1674
1832
  const log5 = logger.child("llm:anthropic");
1675
- log5.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
1833
+ log5.debug(`call: model=${model} jsonSchema=- msgLen=${options.message.length}`);
1676
1834
  const outputCap = 4096;
1677
1835
  const body = {
1678
1836
  model,
@@ -1696,21 +1854,27 @@ class AnthropicExecutor extends LLMExecutor {
1696
1854
  if (!text) {
1697
1855
  throw new Error("Empty response from model");
1698
1856
  }
1699
- return jsonMode ? parseModelJson(text) : text;
1857
+ return text;
1700
1858
  }
1701
1859
  async chatWithTools(model, options) {
1702
1860
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
1703
1861
  const log5 = logger.child("llm:anthropic");
1704
1862
  log5.debug(`chatWithTools: model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
1705
- const { system, msgs } = toAnthropicMessages(options.messages);
1863
+ const { system: system2, msgs: msgs2 } = toAnthropicMessages(options.messages);
1706
1864
  const outputCap = 4096;
1707
1865
  const body = {
1708
1866
  model,
1709
1867
  max_tokens: outputCap,
1710
- system: system ?? options.instruction,
1711
- messages: msgs,
1868
+ system: system2 ?? options.instruction,
1869
+ messages: msgs2,
1712
1870
  tools: options.tools.map(toAnthropicTool)
1713
1871
  };
1872
+ if (options.toolChoice) {
1873
+ body["tool_choice"] = {
1874
+ type: "tool",
1875
+ name: options.toolChoice.name
1876
+ };
1877
+ }
1714
1878
  if (reasoning !== "none") {
1715
1879
  const budget = REASONING_BUDGET[reasoning];
1716
1880
  body["max_tokens"] = budget + outputCap;
@@ -1789,58 +1953,6 @@ function signRequest(opts) {
1789
1953
  headers["X-Amz-Security-Token"] = opts.sessionToken;
1790
1954
  return headers;
1791
1955
  }
1792
- function toAnthropicMessages2(messages) {
1793
- let system;
1794
- const msgs = [];
1795
- for (const m of messages) {
1796
- if (m.role === "system") {
1797
- system = (system ? system + `
1798
-
1799
- ` : "") + m.content;
1800
- continue;
1801
- }
1802
- if (m.role === "user") {
1803
- msgs.push({ role: "user", content: [{ type: "text", text: m.content }] });
1804
- continue;
1805
- }
1806
- if (m.role === "assistant") {
1807
- const blocks = [];
1808
- if (m.content)
1809
- blocks.push({ type: "text", text: m.content });
1810
- if (m.toolCalls) {
1811
- for (const c of m.toolCalls) {
1812
- let input = {};
1813
- try {
1814
- input = c.function.arguments ? JSON.parse(c.function.arguments) : {};
1815
- } catch {
1816
- input = {};
1817
- }
1818
- blocks.push({
1819
- type: "tool_use",
1820
- id: c.id,
1821
- name: c.function.name,
1822
- input
1823
- });
1824
- }
1825
- }
1826
- msgs.push({ role: "assistant", content: blocks });
1827
- continue;
1828
- }
1829
- if (m.role === "tool") {
1830
- msgs.push({
1831
- role: "user",
1832
- content: [
1833
- {
1834
- type: "tool_result",
1835
- tool_use_id: m.toolCallId,
1836
- content: [{ type: "text", text: m.content }]
1837
- }
1838
- ]
1839
- });
1840
- }
1841
- }
1842
- return { system, msgs };
1843
- }
1844
1956
  function toAnthropicTool2(t) {
1845
1957
  return {
1846
1958
  name: t.name,
@@ -1909,9 +2021,24 @@ class BedrockExecutor extends LLMExecutor {
1909
2021
  return JSON.parse(responseRaw);
1910
2022
  }
1911
2023
  async call(model, options) {
1912
- const jsonMode = "jsonSchemaName" in options;
1913
2024
  const log5 = logger.child("llm:bedrock");
1914
- log5.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
2025
+ if ("jsonSchemaName" in options) {
2026
+ log5.debug(`call: model=${model} jsonSchema=${options.jsonSchemaName} via tool`);
2027
+ const { toolName, tool, instruction } = buildStructuredJsonRequest({
2028
+ instruction: options.instruction,
2029
+ jsonSchemaName: options.jsonSchemaName,
2030
+ jsonSchema: options.jsonSchema
2031
+ });
2032
+ const choice = await this.chatWithTools(model, {
2033
+ caller: options.caller ?? options.jsonSchemaName,
2034
+ instruction,
2035
+ messages: [{ role: "user", content: options.message }],
2036
+ tools: [tool],
2037
+ toolChoice: { type: "tool", name: toolName }
2038
+ });
2039
+ return parseStructuredJsonResult(choice, toolName);
2040
+ }
2041
+ log5.debug(`call: model=${model} jsonSchema=- msgLen=${options.message.length}`);
1915
2042
  if (!model.startsWith("anthropic.") && !model.startsWith("us.anthropic.")) {
1916
2043
  throw new Error(`bedrock provider currently only supports Anthropic models (got ${model})`);
1917
2044
  }
@@ -1926,7 +2053,7 @@ class BedrockExecutor extends LLMExecutor {
1926
2053
  if (!text) {
1927
2054
  throw new Error("Empty response from model");
1928
2055
  }
1929
- return jsonMode ? parseModelJson(text) : text;
2056
+ return text;
1930
2057
  }
1931
2058
  async chatWithTools(model, options) {
1932
2059
  const log5 = logger.child("llm:bedrock");
@@ -1934,7 +2061,6 @@ class BedrockExecutor extends LLMExecutor {
1934
2061
  if (!model.startsWith("anthropic.") && !model.startsWith("us.anthropic.")) {
1935
2062
  throw new Error(`bedrock provider currently only supports Anthropic models (got ${model})`);
1936
2063
  }
1937
- const { system, msgs } = toAnthropicMessages2(options.messages);
1938
2064
  const body = {
1939
2065
  anthropic_version: "bedrock-2023-05-31",
1940
2066
  max_tokens: 4096,
@@ -1942,6 +2068,12 @@ class BedrockExecutor extends LLMExecutor {
1942
2068
  messages: msgs,
1943
2069
  tools: options.tools.map(toAnthropicTool2)
1944
2070
  };
2071
+ if (options.toolChoice) {
2072
+ body.tool_choice = {
2073
+ type: "tool",
2074
+ name: options.toolChoice.name
2075
+ };
2076
+ }
1945
2077
  const data = await this.invoke(model, body, resolveLlmCaller(options));
1946
2078
  const { text, toolCalls } = extractAnthropicContent(data);
1947
2079
  return { message: { content: text || undefined, toolCalls } };
@@ -2208,9 +2340,23 @@ class GitLabDuoExecutor extends LLMExecutor {
2208
2340
  return JSON.parse(responseRaw);
2209
2341
  }
2210
2342
  async call(model, options) {
2211
- const jsonMode = "jsonSchemaName" in options;
2212
2343
  const log5 = logger.child("llm:gitlab-duo");
2213
- log5.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
2344
+ if ("jsonSchemaName" in options) {
2345
+ log5.debug(`call: model=${model} jsonSchema=${options.jsonSchemaName} via tool`);
2346
+ const { toolName, tool, instruction } = buildStructuredJsonRequest({
2347
+ instruction: options.instruction,
2348
+ jsonSchemaName: options.jsonSchemaName,
2349
+ jsonSchema: options.jsonSchema
2350
+ });
2351
+ const choice = await this.chatWithTools(model, {
2352
+ caller: options.caller ?? options.jsonSchemaName,
2353
+ instruction,
2354
+ messages: [{ role: "user", content: options.message }],
2355
+ tools: [tool]
2356
+ });
2357
+ return parseStructuredJsonResult(choice, toolName);
2358
+ }
2359
+ log5.debug(`call: model=${model} jsonSchema=- msgLen=${options.message.length}`);
2214
2360
  const body = {
2215
2361
  model,
2216
2362
  messages: [
@@ -2226,7 +2372,7 @@ class GitLabDuoExecutor extends LLMExecutor {
2226
2372
  if (!content) {
2227
2373
  throw new Error("Empty response from model");
2228
2374
  }
2229
- return jsonMode ? parseModelJson(content) : content;
2375
+ return content;
2230
2376
  }
2231
2377
  async chatWithTools(model, options) {
2232
2378
  const log5 = logger.child("llm:gitlab-duo");
@@ -2325,10 +2471,25 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2325
2471
  return JSON.parse(responseRaw);
2326
2472
  }
2327
2473
  async call(model, options) {
2328
- const jsonMode = "jsonSchemaName" in options;
2329
- const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
2330
2474
  const log5 = logger.child("llm:snowflake-cortex");
2331
- log5.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
2475
+ if ("jsonSchemaName" in options) {
2476
+ log5.debug(`call: model=${model} jsonSchema=${options.jsonSchemaName} via tool`);
2477
+ const { toolName, tool, instruction } = buildStructuredJsonRequest({
2478
+ instruction: options.instruction,
2479
+ jsonSchemaName: options.jsonSchemaName,
2480
+ jsonSchema: options.jsonSchema
2481
+ });
2482
+ const choice = await this.chatWithTools(model, {
2483
+ caller: options.caller ?? options.jsonSchemaName,
2484
+ instruction,
2485
+ messages: [{ role: "user", content: options.message }],
2486
+ tools: [tool],
2487
+ reasoningEffort: "none"
2488
+ });
2489
+ return parseStructuredJsonResult(choice, toolName);
2490
+ }
2491
+ const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
2492
+ log5.debug(`call: model=${model} jsonSchema=- msgLen=${options.message.length}`);
2332
2493
  const body = {
2333
2494
  model,
2334
2495
  messages: [
@@ -2347,7 +2508,7 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2347
2508
  if (!content) {
2348
2509
  throw new Error("Empty response from model");
2349
2510
  }
2350
- return jsonMode ? parseModelJson(content) : content;
2511
+ return content;
2351
2512
  }
2352
2513
  async chatWithTools(model, options) {
2353
2514
  const log5 = logger.child("llm:snowflake-cortex");
@@ -3016,75 +3177,46 @@ class Brain {
3016
3177
  content: userPrompt
3017
3178
  }
3018
3179
  ];
3019
- for (let step = 0;step < maxSteps; step += 1) {
3020
- log7.debug(`sendMessage: step ${step + 1}/${maxSteps} → model`);
3021
- let choice;
3022
- try {
3023
- choice = await llm.chatWithTools(llm.models.conversation, {
3024
- caller: initiate ? "start-conversation" : "send-message",
3025
- instruction: `${this.brainbase.baseSystemPrompt}
3180
+ try {
3181
+ await llm.chatWithToolExecution(llm.models.conversation, {
3182
+ caller: initiate ? "start-conversation" : "send-message",
3183
+ instruction: `${this.brainbase.baseSystemPrompt}
3026
3184
 
3027
3185
  ${instruction}`,
3028
- messages,
3029
- tools
3030
- });
3031
- } catch (error) {
3032
- const reason = error instanceof Error ? error.message : String(error);
3033
- logger.error(`sendMessage: LLM call failed at step ${step}: ${reason}`);
3034
- return replyMessages;
3035
- }
3036
- const assistantMessage = choice.message;
3037
- const toolCalls = assistantMessage.toolCalls ?? [];
3038
- const hasContent = typeof assistantMessage.content === "string" && assistantMessage.content.length > 0;
3039
- log7.debug(`sendMessage: step ${step + 1} → toolCalls=${toolCalls.length} hasContent=${hasContent}`);
3040
- if (toolCalls.length === 0) {
3041
- log7.debug(`sendMessage: model returned no tool calls; finalising with ${replyMessages.length} replies`);
3042
- return replyMessages;
3043
- }
3044
- messages.push(stripAssistantForHistory(assistantMessage));
3045
- for (const call of toolCalls) {
3046
- if (call.function.name === "addReplyMessage") {
3047
- const content = parseAddReplyMessageArguments(call.function.arguments);
3048
- if (content !== null) {
3049
- log7.debug(`sendMessage: addReplyMessage[${replyMessages.length}] (${content.length} chars)`);
3050
- send(content);
3051
- replyMessages.push(content);
3052
- } else {
3186
+ messages,
3187
+ tools,
3188
+ maxSteps,
3189
+ executeTool: async (call) => {
3190
+ if (call.function.name === "addReplyMessage") {
3191
+ const content = parseAddReplyMessageArguments(call.function.arguments);
3192
+ if (content !== null) {
3193
+ log7.debug(`sendMessage: addReplyMessage[${replyMessages.length}] (${content.length} chars)`);
3194
+ await send(content);
3195
+ replyMessages.push(content);
3196
+ return JSON.stringify({
3197
+ ok: true,
3198
+ index: replyMessages.length - 1
3199
+ });
3200
+ }
3053
3201
  log7.debug(`sendMessage: addReplyMessage rejected (invalid arguments: ${call.function.arguments})`);
3202
+ return JSON.stringify({ ok: false, error: "invalid arguments" });
3054
3203
  }
3055
- messages.push({
3056
- role: "tool",
3057
- toolCallId: call.id,
3058
- content: content === null ? JSON.stringify({ ok: false, error: "invalid arguments" }) : JSON.stringify({ ok: true, index: replyMessages.length - 1 })
3059
- });
3060
- continue;
3061
- }
3062
- if (call.function.name === "searchMemory") {
3063
- log7.debug(`sendMessage: searchMemory tool call`);
3064
- const result = await this.executeSearchTool(call.function.arguments);
3065
- messages.push({
3066
- role: "tool",
3067
- toolCallId: call.id,
3068
- content: result
3069
- });
3070
- continue;
3071
- }
3072
- log7.debug(`sendMessage: unknown tool "${call.function.name}"`);
3073
- messages.push({
3074
- role: "tool",
3075
- toolCallId: call.id,
3076
- content: JSON.stringify({
3204
+ if (call.function.name === "searchMemory") {
3205
+ log7.debug(`sendMessage: searchMemory tool call`);
3206
+ return this.executeSearchTool(call.function.arguments);
3207
+ }
3208
+ log7.debug(`sendMessage: unknown tool "${call.function.name}"`);
3209
+ return JSON.stringify({
3077
3210
  ok: false,
3078
3211
  error: `Unknown tool: ${call.function.name}`
3079
- })
3080
- });
3081
- }
3082
- if (!hasContent && toolCalls.every((c) => c.function.name === "searchMemory")) {
3083
- log7.debug(`sendMessage: step was pure searchMemory, looping back`);
3084
- continue;
3085
- }
3212
+ });
3213
+ }
3214
+ });
3215
+ } catch (error) {
3216
+ const reason = error instanceof Error ? error.message : String(error);
3217
+ logger.error(`sendMessage: LLM call failed: ${reason}`);
3086
3218
  }
3087
- logger.warn(`sendMessage: reached maxSteps (${maxSteps}) without final reply`);
3219
+ log7.debug(`sendMessage: done with ${replyMessages.length} replies`);
3088
3220
  return replyMessages;
3089
3221
  }
3090
3222
  async buildMemoryBlock() {
@@ -3336,13 +3468,6 @@ function parseSearchArguments(json) {
3336
3468
  }
3337
3469
  return null;
3338
3470
  }
3339
- function stripAssistantForHistory(message) {
3340
- return {
3341
- role: "assistant",
3342
- content: message.content,
3343
- toolCalls: message.toolCalls
3344
- };
3345
- }
3346
3471
 
3347
3472
  // src/channel/discord.ts
3348
3473
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p-sw/brainbox",
3
- "version": "0.1.2-alpha.11",
3
+ "version": "0.1.2-alpha.13",
4
4
  "module": "dist/index.js",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",