@p-sw/brainbox 0.1.2-alpha.9 → 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
|
@@ -489,15 +489,100 @@ function stripThinkTags(content) {
|
|
|
489
489
|
}
|
|
490
490
|
function parseModelJson(content) {
|
|
491
491
|
const cleaned = stripThinkTags(content);
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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,56 @@ 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
|
+
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
|
+
}
|
|
515
650
|
static providers = [];
|
|
516
651
|
static registerProvider(p) {
|
|
517
652
|
LLMExecutor.providers.push(p);
|
|
@@ -550,7 +685,7 @@ class LLMExecutor {
|
|
|
550
685
|
});
|
|
551
686
|
};
|
|
552
687
|
if (conv.provider === id.provider) {
|
|
553
|
-
return
|
|
688
|
+
return build(conv.provider);
|
|
554
689
|
}
|
|
555
690
|
const modelToProvider = {
|
|
556
691
|
[conv.model]: conv.provider,
|
|
@@ -561,7 +696,7 @@ class LLMExecutor {
|
|
|
561
696
|
[id.provider]: build(id.provider)
|
|
562
697
|
};
|
|
563
698
|
const fallback = instances[conv.provider];
|
|
564
|
-
return
|
|
699
|
+
return new class extends LLMExecutor {
|
|
565
700
|
providerName = "dispatch";
|
|
566
701
|
models = {
|
|
567
702
|
conversation: conv.model,
|
|
@@ -575,126 +710,21 @@ class LLMExecutor {
|
|
|
575
710
|
const exec = instances[modelToProvider[model] ?? ""] ?? fallback;
|
|
576
711
|
return exec.chatWithTools(model, options);
|
|
577
712
|
}
|
|
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);
|
|
713
|
+
};
|
|
588
714
|
}
|
|
589
715
|
}
|
|
590
|
-
function
|
|
716
|
+
function resolveLlmCaller(options, fallback = "llm") {
|
|
591
717
|
return options.caller ?? options.jsonSchemaName ?? fallback;
|
|
592
718
|
}
|
|
593
|
-
function
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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(`
|
|
719
|
+
function logLlmWire(caller, request, response) {
|
|
720
|
+
if (!isLlmLogEnabled())
|
|
721
|
+
return;
|
|
722
|
+
writeLlmExchange(caller, `======== REQUEST ========
|
|
723
|
+
${request}
|
|
724
|
+
|
|
725
|
+
======== RESPONSE ========
|
|
726
|
+
${response}
|
|
666
727
|
`);
|
|
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
728
|
}
|
|
699
729
|
function listProviderNames() {
|
|
700
730
|
return LLMExecutor.listProviderNames();
|
|
@@ -764,27 +794,27 @@ class OpenRouterExecutor extends LLMExecutor {
|
|
|
764
794
|
async call(model, options) {
|
|
765
795
|
const jsonMode = "jsonSchemaName" in options;
|
|
766
796
|
log3.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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));
|
|
788
818
|
const raw = result.choices[0]?.message?.content;
|
|
789
819
|
const content = typeof raw === "string" ? stripThinkTags(raw) : raw;
|
|
790
820
|
if (!content) {
|
|
@@ -796,22 +826,22 @@ class OpenRouterExecutor extends LLMExecutor {
|
|
|
796
826
|
}
|
|
797
827
|
async chatWithTools(model, options) {
|
|
798
828
|
log3.debug(`chatWithTools: model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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));
|
|
815
845
|
const choice = result.choices[0];
|
|
816
846
|
if (!choice) {
|
|
817
847
|
log3.debug(`chatWithTools: no choice in response`);
|
|
@@ -886,6 +916,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
|
|
|
886
916
|
chatPath;
|
|
887
917
|
noBearerPrefix;
|
|
888
918
|
reasoningEffortInQuery;
|
|
919
|
+
supportsResponseFormat;
|
|
889
920
|
constructor(opts) {
|
|
890
921
|
super();
|
|
891
922
|
this.providerName = opts.providerName;
|
|
@@ -895,6 +926,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
|
|
|
895
926
|
this.chatPath = opts.chatPath ?? "/chat/completions";
|
|
896
927
|
this.noBearerPrefix = opts.noBearerPrefix ?? false;
|
|
897
928
|
this.reasoningEffortInQuery = opts.reasoningEffortInQuery ?? false;
|
|
929
|
+
this.supportsResponseFormat = opts.supportsResponseFormat ?? true;
|
|
898
930
|
this.models = {
|
|
899
931
|
conversation: opts.conversationModel,
|
|
900
932
|
identity: opts.identityModel
|
|
@@ -925,7 +957,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
|
|
|
925
957
|
}
|
|
926
958
|
return url.toString();
|
|
927
959
|
}
|
|
928
|
-
async sendRequest(body, reasoningEffort) {
|
|
960
|
+
async sendRequest(body, reasoningEffort, caller) {
|
|
929
961
|
const modelName = body["model"];
|
|
930
962
|
const modelStr = typeof modelName === "string" ? modelName : "";
|
|
931
963
|
const url = this.buildRequestUrl(modelStr, reasoningEffort);
|
|
@@ -935,24 +967,40 @@ class OpenAICompatibleExecutor extends LLMExecutor {
|
|
|
935
967
|
...authHeader,
|
|
936
968
|
...this.defaultHeaders
|
|
937
969
|
};
|
|
970
|
+
const requestRaw = JSON.stringify(body);
|
|
938
971
|
const res = await fetch(url, {
|
|
939
972
|
method: "POST",
|
|
940
973
|
headers,
|
|
941
|
-
body:
|
|
974
|
+
body: requestRaw
|
|
942
975
|
});
|
|
976
|
+
const responseRaw = await res.text().catch(() => "");
|
|
977
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
943
978
|
if (!res.ok) {
|
|
944
|
-
|
|
945
|
-
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)}`);
|
|
946
980
|
throw new Error(`${this.providerName} request failed: ${res.status} ${res.statusText}`);
|
|
947
981
|
}
|
|
948
|
-
|
|
982
|
+
let data;
|
|
983
|
+
try {
|
|
984
|
+
data = JSON.parse(responseRaw);
|
|
985
|
+
} catch {
|
|
986
|
+
throw new Error(`${this.providerName}: invalid JSON response`);
|
|
987
|
+
}
|
|
949
988
|
if (data.error) {
|
|
950
989
|
log4.error(`${this.providerName}: API error ${data.error.type ?? ""} ${data.error.message ?? ""}`);
|
|
951
990
|
throw new Error(`${this.providerName} API error: ${data.error.message ?? "unknown"}`);
|
|
952
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
|
+
}
|
|
953
998
|
return data;
|
|
954
999
|
}
|
|
955
1000
|
async call(model, options) {
|
|
1001
|
+
if ("jsonSchemaName" in options && !this.supportsResponseFormat) {
|
|
1002
|
+
return this.callJsonViaTool(model, options);
|
|
1003
|
+
}
|
|
956
1004
|
const jsonMode = "jsonSchemaName" in options;
|
|
957
1005
|
const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
|
|
958
1006
|
log4.debug(`call: provider=${this.providerName} model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
|
|
@@ -965,16 +1013,32 @@ class OpenAICompatibleExecutor extends LLMExecutor {
|
|
|
965
1013
|
responseFormat: jsonMode ? buildResponseFormat(options.jsonSchemaName, options.jsonSchema) : undefined,
|
|
966
1014
|
reasoningEffort: reasoning
|
|
967
1015
|
});
|
|
968
|
-
const data = await this.sendRequest(body, options.reasoningEffort);
|
|
969
|
-
const
|
|
1016
|
+
const data = await this.sendRequest(body, options.reasoningEffort, resolveLlmCaller(options));
|
|
1017
|
+
const choice = data.choices?.[0];
|
|
1018
|
+
const raw = choice?.message?.content;
|
|
970
1019
|
const content = typeof raw === "string" ? stripThinkTags(raw) : raw;
|
|
971
1020
|
if (!content) {
|
|
972
|
-
|
|
973
|
-
|
|
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");
|
|
974
1025
|
}
|
|
975
1026
|
log4.debug(`call: response ${content.length} chars`);
|
|
976
1027
|
return jsonMode ? parseModelJson(content) : content;
|
|
977
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
|
+
}
|
|
978
1042
|
async chatWithTools(model, options) {
|
|
979
1043
|
const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
|
|
980
1044
|
log4.debug(`chatWithTools: provider=${this.providerName} model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
|
|
@@ -988,7 +1052,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
|
|
|
988
1052
|
parallelToolCalls: options.parallelToolCalls ?? false,
|
|
989
1053
|
reasoningEffort: reasoning
|
|
990
1054
|
});
|
|
991
|
-
const data = await this.sendRequest(body, options.reasoningEffort);
|
|
1055
|
+
const data = await this.sendRequest(body, options.reasoningEffort, resolveLlmCaller(options));
|
|
992
1056
|
const choice = data.choices?.[0];
|
|
993
1057
|
if (!choice) {
|
|
994
1058
|
log4.debug(`chatWithTools: no choice in response`);
|
|
@@ -1290,10 +1354,15 @@ class ZenMuxExecutor extends OpenAICompatibleExecutor {
|
|
|
1290
1354
|
// src/provider/providers/minimax.ts
|
|
1291
1355
|
class MiniMaxBase extends OpenAICompatibleExecutor {
|
|
1292
1356
|
buildBody(opts) {
|
|
1357
|
+
const jsonMode = opts.responseFormat !== undefined;
|
|
1293
1358
|
const body = super.buildBody(opts);
|
|
1294
1359
|
delete body["reasoning_effort"];
|
|
1360
|
+
delete body["response_format"];
|
|
1361
|
+
delete body["parallel_tool_calls"];
|
|
1295
1362
|
body["reasoning_split"] = true;
|
|
1296
|
-
body["
|
|
1363
|
+
body["max_completion_tokens"] = 16384;
|
|
1364
|
+
const wantThink = !jsonMode && opts.reasoningEffort !== undefined && opts.reasoningEffort !== "none";
|
|
1365
|
+
body["thinking"] = wantThink ? { type: "adaptive" } : { type: "disabled" };
|
|
1297
1366
|
return body;
|
|
1298
1367
|
}
|
|
1299
1368
|
}
|
|
@@ -1303,7 +1372,8 @@ function minimaxOpts(providerName, baseURL, opts) {
|
|
|
1303
1372
|
baseURL,
|
|
1304
1373
|
apiKey: opts.apiKey,
|
|
1305
1374
|
conversationModel: opts.conversationModel,
|
|
1306
|
-
identityModel: opts.identityModel
|
|
1375
|
+
identityModel: opts.identityModel,
|
|
1376
|
+
supportsResponseFormat: false
|
|
1307
1377
|
};
|
|
1308
1378
|
}
|
|
1309
1379
|
|
|
@@ -1379,7 +1449,8 @@ class LmStudioExecutor extends OpenAICompatibleExecutor {
|
|
|
1379
1449
|
baseURL: "http://127.0.0.1:1234/v1",
|
|
1380
1450
|
apiKey: opts.apiKey || "lm-studio",
|
|
1381
1451
|
conversationModel: opts.conversationModel,
|
|
1382
|
-
identityModel: opts.identityModel
|
|
1452
|
+
identityModel: opts.identityModel,
|
|
1453
|
+
supportsResponseFormat: false
|
|
1383
1454
|
});
|
|
1384
1455
|
}
|
|
1385
1456
|
}
|
|
@@ -1392,7 +1463,8 @@ class OllamaExecutor extends OpenAICompatibleExecutor {
|
|
|
1392
1463
|
baseURL: "http://127.0.0.1:11434/v1",
|
|
1393
1464
|
apiKey: opts.apiKey || "ollama",
|
|
1394
1465
|
conversationModel: opts.conversationModel,
|
|
1395
|
-
identityModel: opts.identityModel
|
|
1466
|
+
identityModel: opts.identityModel,
|
|
1467
|
+
supportsResponseFormat: false
|
|
1396
1468
|
});
|
|
1397
1469
|
}
|
|
1398
1470
|
}
|
|
@@ -1418,7 +1490,8 @@ class LlamaCppExecutor extends OpenAICompatibleExecutor {
|
|
|
1418
1490
|
baseURL: "http://127.0.0.1:8080/v1",
|
|
1419
1491
|
apiKey: opts.apiKey || "llama.cpp",
|
|
1420
1492
|
conversationModel: opts.conversationModel,
|
|
1421
|
-
identityModel: opts.identityModel
|
|
1493
|
+
identityModel: opts.identityModel,
|
|
1494
|
+
supportsResponseFormat: false
|
|
1422
1495
|
});
|
|
1423
1496
|
}
|
|
1424
1497
|
}
|
|
@@ -1501,21 +1574,23 @@ class CloudflareWorkersExecutor extends LLMExecutor {
|
|
|
1501
1574
|
const accountId = readAuthString(opts.auth, "accountId", "CLOUDFLARE_ACCOUNT_ID");
|
|
1502
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";
|
|
1503
1576
|
}
|
|
1504
|
-
async run(model, body) {
|
|
1577
|
+
async run(model, body, caller) {
|
|
1505
1578
|
const url = `${this.baseURL}/${encodeURIComponent(model)}`;
|
|
1579
|
+
const requestRaw = JSON.stringify(body);
|
|
1506
1580
|
const res = await fetch(url, {
|
|
1507
1581
|
method: "POST",
|
|
1508
1582
|
headers: {
|
|
1509
1583
|
Authorization: `Bearer ${this.apiKey}`,
|
|
1510
1584
|
"Content-Type": "application/json"
|
|
1511
1585
|
},
|
|
1512
|
-
body:
|
|
1586
|
+
body: requestRaw
|
|
1513
1587
|
});
|
|
1588
|
+
const responseRaw = await res.text().catch(() => "");
|
|
1589
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
1514
1590
|
if (!res.ok) {
|
|
1515
|
-
|
|
1516
|
-
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)}`);
|
|
1517
1592
|
}
|
|
1518
|
-
return
|
|
1593
|
+
return JSON.parse(responseRaw);
|
|
1519
1594
|
}
|
|
1520
1595
|
async call(model, options) {
|
|
1521
1596
|
const jsonMode = "jsonSchemaName" in options;
|
|
@@ -1536,7 +1611,7 @@ class CloudflareWorkersExecutor extends LLMExecutor {
|
|
|
1536
1611
|
if (reasoning !== "none") {
|
|
1537
1612
|
body["reasoning_effort"] = reasoning;
|
|
1538
1613
|
}
|
|
1539
|
-
const data = await this.run(model, body);
|
|
1614
|
+
const data = await this.run(model, body, resolveLlmCaller(options));
|
|
1540
1615
|
if (data.errors && data.errors.length > 0) {
|
|
1541
1616
|
throw new Error(`cloudflare-workers API error: ${data.errors.map((e) => e.message).join("; ")}`);
|
|
1542
1617
|
}
|
|
@@ -1565,7 +1640,7 @@ class CloudflareWorkersExecutor extends LLMExecutor {
|
|
|
1565
1640
|
if (reasoning !== "none") {
|
|
1566
1641
|
body["reasoning_effort"] = reasoning;
|
|
1567
1642
|
}
|
|
1568
|
-
const data = await this.run(model, body);
|
|
1643
|
+
const data = await this.run(model, body, resolveLlmCaller(options));
|
|
1569
1644
|
if (data.errors && data.errors.length > 0) {
|
|
1570
1645
|
throw new Error(`cloudflare-workers API error: ${data.errors.map((e) => e.message).join("; ")}`);
|
|
1571
1646
|
}
|
|
@@ -1655,17 +1730,17 @@ var AnthropicAuthSchema = z4.object({
|
|
|
1655
1730
|
apiVersion: z4.string().optional()
|
|
1656
1731
|
}).loose();
|
|
1657
1732
|
function toAnthropicMessages(messages) {
|
|
1658
|
-
let
|
|
1659
|
-
const
|
|
1733
|
+
let system2;
|
|
1734
|
+
const msgs2 = [];
|
|
1660
1735
|
for (const m of messages) {
|
|
1661
1736
|
if (m.role === "system") {
|
|
1662
|
-
|
|
1737
|
+
system2 = (system2 ? system2 + `
|
|
1663
1738
|
|
|
1664
1739
|
` : "") + m.content;
|
|
1665
1740
|
continue;
|
|
1666
1741
|
}
|
|
1667
1742
|
if (m.role === "user") {
|
|
1668
|
-
|
|
1743
|
+
msgs2.push({ role: "user", content: m.content });
|
|
1669
1744
|
continue;
|
|
1670
1745
|
}
|
|
1671
1746
|
if (m.role === "assistant") {
|
|
@@ -1688,11 +1763,11 @@ function toAnthropicMessages(messages) {
|
|
|
1688
1763
|
});
|
|
1689
1764
|
}
|
|
1690
1765
|
}
|
|
1691
|
-
|
|
1766
|
+
msgs2.push({ role: "assistant", content: blocks });
|
|
1692
1767
|
continue;
|
|
1693
1768
|
}
|
|
1694
1769
|
if (m.role === "tool") {
|
|
1695
|
-
|
|
1770
|
+
msgs2.push({
|
|
1696
1771
|
role: "user",
|
|
1697
1772
|
content: [
|
|
1698
1773
|
{
|
|
@@ -1704,7 +1779,7 @@ function toAnthropicMessages(messages) {
|
|
|
1704
1779
|
});
|
|
1705
1780
|
}
|
|
1706
1781
|
}
|
|
1707
|
-
return { system, msgs };
|
|
1782
|
+
return { system: system2, msgs: msgs2 };
|
|
1708
1783
|
}
|
|
1709
1784
|
function toAnthropicTool(t) {
|
|
1710
1785
|
return {
|
|
@@ -1732,7 +1807,8 @@ class AnthropicExecutor extends LLMExecutor {
|
|
|
1732
1807
|
this.baseURL = (extra.baseURL ?? readAuthString(opts.auth, "baseURL", "ANTHROPIC_BASE_URL") ?? "https://api.anthropic.com").replace(/\/v1\/?$/, "");
|
|
1733
1808
|
this.apiVersion = extra.apiVersion ?? "2023-06-01";
|
|
1734
1809
|
}
|
|
1735
|
-
async send(body) {
|
|
1810
|
+
async send(body, caller) {
|
|
1811
|
+
const requestRaw = JSON.stringify(body);
|
|
1736
1812
|
const res = await fetch(`${this.baseURL}/v1/messages`, {
|
|
1737
1813
|
method: "POST",
|
|
1738
1814
|
headers: {
|
|
@@ -1740,19 +1816,35 @@ class AnthropicExecutor extends LLMExecutor {
|
|
|
1740
1816
|
"anthropic-version": this.apiVersion,
|
|
1741
1817
|
"Content-Type": "application/json"
|
|
1742
1818
|
},
|
|
1743
|
-
body:
|
|
1819
|
+
body: requestRaw
|
|
1744
1820
|
});
|
|
1821
|
+
const responseRaw = await res.text().catch(() => "");
|
|
1822
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
1745
1823
|
if (!res.ok) {
|
|
1746
|
-
|
|
1747
|
-
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)}`);
|
|
1748
1825
|
}
|
|
1749
|
-
return
|
|
1826
|
+
return JSON.parse(responseRaw);
|
|
1750
1827
|
}
|
|
1751
1828
|
async call(model, options) {
|
|
1752
|
-
|
|
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
|
+
}
|
|
1753
1845
|
const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
|
|
1754
1846
|
const log5 = logger.child("llm:anthropic");
|
|
1755
|
-
log5.debug(`call: model=${model} jsonSchema
|
|
1847
|
+
log5.debug(`call: model=${model} jsonSchema=- msgLen=${options.message.length}`);
|
|
1756
1848
|
const outputCap = 4096;
|
|
1757
1849
|
const body = {
|
|
1758
1850
|
model,
|
|
@@ -1768,7 +1860,7 @@ class AnthropicExecutor extends LLMExecutor {
|
|
|
1768
1860
|
budget_tokens: budget
|
|
1769
1861
|
};
|
|
1770
1862
|
}
|
|
1771
|
-
const data = await this.send(body);
|
|
1863
|
+
const data = await this.send(body, resolveLlmCaller(options));
|
|
1772
1864
|
if (data.error) {
|
|
1773
1865
|
throw new Error(`anthropic API error: ${data.error.message ?? "unknown"}`);
|
|
1774
1866
|
}
|
|
@@ -1776,21 +1868,27 @@ class AnthropicExecutor extends LLMExecutor {
|
|
|
1776
1868
|
if (!text) {
|
|
1777
1869
|
throw new Error("Empty response from model");
|
|
1778
1870
|
}
|
|
1779
|
-
return
|
|
1871
|
+
return text;
|
|
1780
1872
|
}
|
|
1781
1873
|
async chatWithTools(model, options) {
|
|
1782
1874
|
const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
|
|
1783
1875
|
const log5 = logger.child("llm:anthropic");
|
|
1784
1876
|
log5.debug(`chatWithTools: model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
|
|
1785
|
-
const { system, msgs } = toAnthropicMessages(options.messages);
|
|
1877
|
+
const { system: system2, msgs: msgs2 } = toAnthropicMessages(options.messages);
|
|
1786
1878
|
const outputCap = 4096;
|
|
1787
1879
|
const body = {
|
|
1788
1880
|
model,
|
|
1789
1881
|
max_tokens: outputCap,
|
|
1790
|
-
system:
|
|
1791
|
-
messages:
|
|
1882
|
+
system: system2 ?? options.instruction,
|
|
1883
|
+
messages: msgs2,
|
|
1792
1884
|
tools: options.tools.map(toAnthropicTool)
|
|
1793
1885
|
};
|
|
1886
|
+
if (options.toolChoice) {
|
|
1887
|
+
body["tool_choice"] = {
|
|
1888
|
+
type: "tool",
|
|
1889
|
+
name: options.toolChoice.name
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1794
1892
|
if (reasoning !== "none") {
|
|
1795
1893
|
const budget = REASONING_BUDGET[reasoning];
|
|
1796
1894
|
body["max_tokens"] = budget + outputCap;
|
|
@@ -1799,7 +1897,7 @@ class AnthropicExecutor extends LLMExecutor {
|
|
|
1799
1897
|
budget_tokens: budget
|
|
1800
1898
|
};
|
|
1801
1899
|
}
|
|
1802
|
-
const data = await this.send(body);
|
|
1900
|
+
const data = await this.send(body, resolveLlmCaller(options));
|
|
1803
1901
|
if (data.error) {
|
|
1804
1902
|
throw new Error(`anthropic API error: ${data.error.message ?? "unknown"}`);
|
|
1805
1903
|
}
|
|
@@ -1869,58 +1967,6 @@ function signRequest(opts) {
|
|
|
1869
1967
|
headers["X-Amz-Security-Token"] = opts.sessionToken;
|
|
1870
1968
|
return headers;
|
|
1871
1969
|
}
|
|
1872
|
-
function toAnthropicMessages2(messages) {
|
|
1873
|
-
let system;
|
|
1874
|
-
const msgs = [];
|
|
1875
|
-
for (const m of messages) {
|
|
1876
|
-
if (m.role === "system") {
|
|
1877
|
-
system = (system ? system + `
|
|
1878
|
-
|
|
1879
|
-
` : "") + m.content;
|
|
1880
|
-
continue;
|
|
1881
|
-
}
|
|
1882
|
-
if (m.role === "user") {
|
|
1883
|
-
msgs.push({ role: "user", content: [{ type: "text", text: m.content }] });
|
|
1884
|
-
continue;
|
|
1885
|
-
}
|
|
1886
|
-
if (m.role === "assistant") {
|
|
1887
|
-
const blocks = [];
|
|
1888
|
-
if (m.content)
|
|
1889
|
-
blocks.push({ type: "text", text: m.content });
|
|
1890
|
-
if (m.toolCalls) {
|
|
1891
|
-
for (const c of m.toolCalls) {
|
|
1892
|
-
let input = {};
|
|
1893
|
-
try {
|
|
1894
|
-
input = c.function.arguments ? JSON.parse(c.function.arguments) : {};
|
|
1895
|
-
} catch {
|
|
1896
|
-
input = {};
|
|
1897
|
-
}
|
|
1898
|
-
blocks.push({
|
|
1899
|
-
type: "tool_use",
|
|
1900
|
-
id: c.id,
|
|
1901
|
-
name: c.function.name,
|
|
1902
|
-
input
|
|
1903
|
-
});
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
msgs.push({ role: "assistant", content: blocks });
|
|
1907
|
-
continue;
|
|
1908
|
-
}
|
|
1909
|
-
if (m.role === "tool") {
|
|
1910
|
-
msgs.push({
|
|
1911
|
-
role: "user",
|
|
1912
|
-
content: [
|
|
1913
|
-
{
|
|
1914
|
-
type: "tool_result",
|
|
1915
|
-
tool_use_id: m.toolCallId,
|
|
1916
|
-
content: [{ type: "text", text: m.content }]
|
|
1917
|
-
}
|
|
1918
|
-
]
|
|
1919
|
-
});
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
return { system, msgs };
|
|
1923
|
-
}
|
|
1924
1970
|
function toAnthropicTool2(t) {
|
|
1925
1971
|
return {
|
|
1926
1972
|
name: t.name,
|
|
@@ -1962,7 +2008,7 @@ class BedrockExecutor extends LLMExecutor {
|
|
|
1962
2008
|
this.sessionToken = readAuthString(opts.auth, "sessionToken", "AWS_SESSION_TOKEN");
|
|
1963
2009
|
this.baseURL = `https://bedrock-runtime.${this.region}.amazonaws.com`;
|
|
1964
2010
|
}
|
|
1965
|
-
async invoke(model, body) {
|
|
2011
|
+
async invoke(model, body, caller) {
|
|
1966
2012
|
const bodyStr = JSON.stringify(body);
|
|
1967
2013
|
const path = `/model/${encodeURIComponent(model)}/invoke`;
|
|
1968
2014
|
const headers = signRequest({
|
|
@@ -1981,16 +2027,32 @@ class BedrockExecutor extends LLMExecutor {
|
|
|
1981
2027
|
headers,
|
|
1982
2028
|
body: bodyStr
|
|
1983
2029
|
});
|
|
2030
|
+
const responseRaw = await res.text().catch(() => "");
|
|
2031
|
+
logLlmWire(caller, bodyStr, responseRaw);
|
|
1984
2032
|
if (!res.ok) {
|
|
1985
|
-
|
|
1986
|
-
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)}`);
|
|
1987
2034
|
}
|
|
1988
|
-
return
|
|
2035
|
+
return JSON.parse(responseRaw);
|
|
1989
2036
|
}
|
|
1990
2037
|
async call(model, options) {
|
|
1991
|
-
const jsonMode = "jsonSchemaName" in options;
|
|
1992
2038
|
const log5 = logger.child("llm:bedrock");
|
|
1993
|
-
|
|
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}`);
|
|
1994
2056
|
if (!model.startsWith("anthropic.") && !model.startsWith("us.anthropic.")) {
|
|
1995
2057
|
throw new Error(`bedrock provider currently only supports Anthropic models (got ${model})`);
|
|
1996
2058
|
}
|
|
@@ -2000,12 +2062,12 @@ class BedrockExecutor extends LLMExecutor {
|
|
|
2000
2062
|
system: options.instruction,
|
|
2001
2063
|
messages: [{ role: "user", content: options.message }]
|
|
2002
2064
|
};
|
|
2003
|
-
const data = await this.invoke(model, body);
|
|
2065
|
+
const data = await this.invoke(model, body, resolveLlmCaller(options));
|
|
2004
2066
|
const { text } = extractAnthropicContent(data);
|
|
2005
2067
|
if (!text) {
|
|
2006
2068
|
throw new Error("Empty response from model");
|
|
2007
2069
|
}
|
|
2008
|
-
return
|
|
2070
|
+
return text;
|
|
2009
2071
|
}
|
|
2010
2072
|
async chatWithTools(model, options) {
|
|
2011
2073
|
const log5 = logger.child("llm:bedrock");
|
|
@@ -2013,7 +2075,6 @@ class BedrockExecutor extends LLMExecutor {
|
|
|
2013
2075
|
if (!model.startsWith("anthropic.") && !model.startsWith("us.anthropic.")) {
|
|
2014
2076
|
throw new Error(`bedrock provider currently only supports Anthropic models (got ${model})`);
|
|
2015
2077
|
}
|
|
2016
|
-
const { system, msgs } = toAnthropicMessages2(options.messages);
|
|
2017
2078
|
const body = {
|
|
2018
2079
|
anthropic_version: "bedrock-2023-05-31",
|
|
2019
2080
|
max_tokens: 4096,
|
|
@@ -2021,7 +2082,13 @@ class BedrockExecutor extends LLMExecutor {
|
|
|
2021
2082
|
messages: msgs,
|
|
2022
2083
|
tools: options.tools.map(toAnthropicTool2)
|
|
2023
2084
|
};
|
|
2024
|
-
|
|
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));
|
|
2025
2092
|
const { text, toolCalls } = extractAnthropicContent(data);
|
|
2026
2093
|
return { message: { content: text || undefined, toolCalls } };
|
|
2027
2094
|
}
|
|
@@ -2137,21 +2204,23 @@ class VertexExecutor extends LLMExecutor {
|
|
|
2137
2204
|
this.region = extra.region ?? readAuthString(opts.auth, "region", "GOOGLE_CLOUD_REGION") ?? "us-central1";
|
|
2138
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";
|
|
2139
2206
|
}
|
|
2140
|
-
async generate(model, body) {
|
|
2207
|
+
async generate(model, body, caller) {
|
|
2141
2208
|
const url = `${this.baseURL}/${encodeURIComponent(model)}:generateContent`;
|
|
2209
|
+
const requestRaw = JSON.stringify(body);
|
|
2142
2210
|
const res = await fetch(url, {
|
|
2143
2211
|
method: "POST",
|
|
2144
2212
|
headers: {
|
|
2145
2213
|
Authorization: `Bearer ${this.apiKey}`,
|
|
2146
2214
|
"Content-Type": "application/json"
|
|
2147
2215
|
},
|
|
2148
|
-
body:
|
|
2216
|
+
body: requestRaw
|
|
2149
2217
|
});
|
|
2218
|
+
const responseRaw = await res.text().catch(() => "");
|
|
2219
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
2150
2220
|
if (!res.ok) {
|
|
2151
|
-
|
|
2152
|
-
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)}`);
|
|
2153
2222
|
}
|
|
2154
|
-
return
|
|
2223
|
+
return JSON.parse(responseRaw);
|
|
2155
2224
|
}
|
|
2156
2225
|
async call(model, options) {
|
|
2157
2226
|
const jsonMode = "jsonSchemaName" in options;
|
|
@@ -2172,7 +2241,7 @@ class VertexExecutor extends LLMExecutor {
|
|
|
2172
2241
|
responseSchema: options.jsonSchema
|
|
2173
2242
|
};
|
|
2174
2243
|
}
|
|
2175
|
-
const data = await this.generate(model, body);
|
|
2244
|
+
const data = await this.generate(model, body, resolveLlmCaller(options));
|
|
2176
2245
|
if (data.error) {
|
|
2177
2246
|
throw new Error(`vertex API error: ${data.error.message ?? "unknown"}`);
|
|
2178
2247
|
}
|
|
@@ -2191,7 +2260,7 @@ class VertexExecutor extends LLMExecutor {
|
|
|
2191
2260
|
systemInstruction: { parts: [{ text: options.instruction }] },
|
|
2192
2261
|
tools: options.tools.length > 0 ? [toGeminiTools(options.tools)] : undefined
|
|
2193
2262
|
};
|
|
2194
|
-
const data = await this.generate(model, body);
|
|
2263
|
+
const data = await this.generate(model, body, resolveLlmCaller(options));
|
|
2195
2264
|
if (data.error) {
|
|
2196
2265
|
throw new Error(`vertex API error: ${data.error.message ?? "unknown"}`);
|
|
2197
2266
|
}
|
|
@@ -2267,25 +2336,41 @@ class GitLabDuoExecutor extends LLMExecutor {
|
|
|
2267
2336
|
const extra = parsed.success ? parsed.data : {};
|
|
2268
2337
|
this.baseURL = (extra.baseURL ?? readAuthString(opts.auth, "baseURL", "GITLAB_BASE_URL") ?? "https://gitlab.com/api/v4/ai/llm/proxy").replace(/\/+$/, "");
|
|
2269
2338
|
}
|
|
2270
|
-
async send(body) {
|
|
2339
|
+
async send(body, caller) {
|
|
2340
|
+
const requestRaw = JSON.stringify(body);
|
|
2271
2341
|
const res = await fetch(this.baseURL, {
|
|
2272
2342
|
method: "POST",
|
|
2273
2343
|
headers: {
|
|
2274
2344
|
Authorization: `Bearer ${this.apiKey}`,
|
|
2275
2345
|
"Content-Type": "application/json"
|
|
2276
2346
|
},
|
|
2277
|
-
body:
|
|
2347
|
+
body: requestRaw
|
|
2278
2348
|
});
|
|
2349
|
+
const responseRaw = await res.text().catch(() => "");
|
|
2350
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
2279
2351
|
if (!res.ok) {
|
|
2280
|
-
|
|
2281
|
-
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)}`);
|
|
2282
2353
|
}
|
|
2283
|
-
return
|
|
2354
|
+
return JSON.parse(responseRaw);
|
|
2284
2355
|
}
|
|
2285
2356
|
async call(model, options) {
|
|
2286
|
-
const jsonMode = "jsonSchemaName" in options;
|
|
2287
2357
|
const log5 = logger.child("llm:gitlab-duo");
|
|
2288
|
-
|
|
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}`);
|
|
2289
2374
|
const body = {
|
|
2290
2375
|
model,
|
|
2291
2376
|
messages: [
|
|
@@ -2293,7 +2378,7 @@ class GitLabDuoExecutor extends LLMExecutor {
|
|
|
2293
2378
|
{ role: "user", content: options.message }
|
|
2294
2379
|
]
|
|
2295
2380
|
};
|
|
2296
|
-
const data = await this.send(body);
|
|
2381
|
+
const data = await this.send(body, resolveLlmCaller(options));
|
|
2297
2382
|
if (data.error) {
|
|
2298
2383
|
throw new Error(`gitlab-duo API error: ${data.error.message ?? "unknown"}`);
|
|
2299
2384
|
}
|
|
@@ -2301,7 +2386,7 @@ class GitLabDuoExecutor extends LLMExecutor {
|
|
|
2301
2386
|
if (!content) {
|
|
2302
2387
|
throw new Error("Empty response from model");
|
|
2303
2388
|
}
|
|
2304
|
-
return
|
|
2389
|
+
return content;
|
|
2305
2390
|
}
|
|
2306
2391
|
async chatWithTools(model, options) {
|
|
2307
2392
|
const log5 = logger.child("llm:gitlab-duo");
|
|
@@ -2314,7 +2399,7 @@ class GitLabDuoExecutor extends LLMExecutor {
|
|
|
2314
2399
|
],
|
|
2315
2400
|
tools: options.tools.map(toTool)
|
|
2316
2401
|
};
|
|
2317
|
-
const data = await this.send(body);
|
|
2402
|
+
const data = await this.send(body, resolveLlmCaller(options));
|
|
2318
2403
|
if (data.error) {
|
|
2319
2404
|
throw new Error(`gitlab-duo API error: ${data.error.message ?? "unknown"}`);
|
|
2320
2405
|
}
|
|
@@ -2382,26 +2467,43 @@ class SnowflakeCortexExecutor extends LLMExecutor {
|
|
|
2382
2467
|
const account = extra.account ?? readAuthString(opts.auth, "account", "SNOWFLAKE_ACCOUNT") ?? "";
|
|
2383
2468
|
this.baseURL = account ? `https://${account}.snowflakecomputing.com/api/v2/cortex/inference:complete` : "https://__account__.snowflakecomputing.com/api/v2/cortex/inference:complete";
|
|
2384
2469
|
}
|
|
2385
|
-
async send(body) {
|
|
2470
|
+
async send(body, caller) {
|
|
2471
|
+
const requestRaw = JSON.stringify(body);
|
|
2386
2472
|
const res = await fetch(this.baseURL, {
|
|
2387
2473
|
method: "POST",
|
|
2388
2474
|
headers: {
|
|
2389
2475
|
Authorization: `Bearer ${this.apiKey}`,
|
|
2390
2476
|
"Content-Type": "application/json"
|
|
2391
2477
|
},
|
|
2392
|
-
body:
|
|
2478
|
+
body: requestRaw
|
|
2393
2479
|
});
|
|
2480
|
+
const responseRaw = await res.text().catch(() => "");
|
|
2481
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
2394
2482
|
if (!res.ok) {
|
|
2395
|
-
|
|
2396
|
-
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)}`);
|
|
2397
2484
|
}
|
|
2398
|
-
return
|
|
2485
|
+
return JSON.parse(responseRaw);
|
|
2399
2486
|
}
|
|
2400
2487
|
async call(model, options) {
|
|
2401
|
-
const jsonMode = "jsonSchemaName" in options;
|
|
2402
|
-
const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
|
|
2403
2488
|
const log5 = logger.child("llm:snowflake-cortex");
|
|
2404
|
-
|
|
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}`);
|
|
2405
2507
|
const body = {
|
|
2406
2508
|
model,
|
|
2407
2509
|
messages: [
|
|
@@ -2412,7 +2514,7 @@ class SnowflakeCortexExecutor extends LLMExecutor {
|
|
|
2412
2514
|
if (reasoning !== "none") {
|
|
2413
2515
|
body["reasoning_effort"] = reasoning;
|
|
2414
2516
|
}
|
|
2415
|
-
const data = await this.send(body);
|
|
2517
|
+
const data = await this.send(body, resolveLlmCaller(options));
|
|
2416
2518
|
if (data.error) {
|
|
2417
2519
|
throw new Error(`snowflake-cortex API error: ${data.error.message ?? "unknown"}`);
|
|
2418
2520
|
}
|
|
@@ -2420,7 +2522,7 @@ class SnowflakeCortexExecutor extends LLMExecutor {
|
|
|
2420
2522
|
if (!content) {
|
|
2421
2523
|
throw new Error("Empty response from model");
|
|
2422
2524
|
}
|
|
2423
|
-
return
|
|
2525
|
+
return content;
|
|
2424
2526
|
}
|
|
2425
2527
|
async chatWithTools(model, options) {
|
|
2426
2528
|
const log5 = logger.child("llm:snowflake-cortex");
|
|
@@ -2433,7 +2535,7 @@ class SnowflakeCortexExecutor extends LLMExecutor {
|
|
|
2433
2535
|
],
|
|
2434
2536
|
tools: options.tools.map(toCortexTool)
|
|
2435
2537
|
};
|
|
2436
|
-
const data = await this.send(body);
|
|
2538
|
+
const data = await this.send(body, resolveLlmCaller(options));
|
|
2437
2539
|
if (data.error) {
|
|
2438
2540
|
throw new Error(`snowflake-cortex API error: ${data.error.message ?? "unknown"}`);
|
|
2439
2541
|
}
|
|
@@ -2822,7 +2924,11 @@ class Brain {
|
|
|
2822
2924
|
async regenerateSchedules() {
|
|
2823
2925
|
const today = new Date;
|
|
2824
2926
|
const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
|
|
2825
|
-
await this.createMonthlySchedule(nextMonth);
|
|
2927
|
+
const monthly = await this.createMonthlySchedule(nextMonth);
|
|
2928
|
+
if (!monthly) {
|
|
2929
|
+
log7.debug(`regenerateSchedules: skip daily — monthly schedule generation failed`);
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2826
2932
|
const tomorrow = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
|
2827
2933
|
await this.createDailySchedule(tomorrow);
|
|
2828
2934
|
await this.createDailySchedule(today);
|
|
@@ -3085,75 +3191,56 @@ class Brain {
|
|
|
3085
3191
|
content: userPrompt
|
|
3086
3192
|
}
|
|
3087
3193
|
];
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
choice = await llm.chatWithTools(llm.models.conversation, {
|
|
3093
|
-
caller: initiate ? "start-conversation" : "send-message",
|
|
3094
|
-
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}
|
|
3095
3198
|
|
|
3096
3199
|
${instruction}`,
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
}
|
|
3113
|
-
messages.push(stripAssistantForHistory(assistantMessage));
|
|
3114
|
-
for (const call of toolCalls) {
|
|
3115
|
-
if (call.function.name === "addReplyMessage") {
|
|
3116
|
-
const content = parseAddReplyMessageArguments(call.function.arguments);
|
|
3117
|
-
if (content !== null) {
|
|
3118
|
-
log7.debug(`sendMessage: addReplyMessage[${replyMessages.length}] (${content.length} chars)`);
|
|
3119
|
-
send(content);
|
|
3120
|
-
replyMessages.push(content);
|
|
3121
|
-
} 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
|
+
}
|
|
3122
3215
|
log7.debug(`sendMessage: addReplyMessage rejected (invalid arguments: ${call.function.arguments})`);
|
|
3216
|
+
return JSON.stringify({ ok: false, error: "invalid arguments" });
|
|
3123
3217
|
}
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
log7.debug(`sendMessage:
|
|
3133
|
-
|
|
3134
|
-
messages.push({
|
|
3135
|
-
role: "tool",
|
|
3136
|
-
toolCallId: call.id,
|
|
3137
|
-
content: result
|
|
3138
|
-
});
|
|
3139
|
-
continue;
|
|
3140
|
-
}
|
|
3141
|
-
log7.debug(`sendMessage: unknown tool "${call.function.name}"`);
|
|
3142
|
-
messages.push({
|
|
3143
|
-
role: "tool",
|
|
3144
|
-
toolCallId: call.id,
|
|
3145
|
-
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({
|
|
3146
3228
|
ok: false,
|
|
3147
3229
|
error: `Unknown tool: ${call.function.name}`
|
|
3148
|
-
})
|
|
3149
|
-
}
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
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}`);
|
|
3155
3242
|
}
|
|
3156
|
-
|
|
3243
|
+
log7.debug(`sendMessage: done with ${replyMessages.length} replies`);
|
|
3157
3244
|
return replyMessages;
|
|
3158
3245
|
}
|
|
3159
3246
|
async buildMemoryBlock() {
|
|
@@ -3356,7 +3443,7 @@ function buildSendMessageTools() {
|
|
|
3356
3443
|
return [
|
|
3357
3444
|
{
|
|
3358
3445
|
name: "addReplyMessage",
|
|
3359
|
-
description: "Append one chat bubble to the reply stream. Call once per bubble you want to send.
|
|
3446
|
+
description: "Append one chat bubble to the reply stream. Call once per bubble you want to send. After at least one successful call, you may end your turn without calling stop.",
|
|
3360
3447
|
parameters: {
|
|
3361
3448
|
type: "object",
|
|
3362
3449
|
additionalProperties: false,
|
|
@@ -3380,6 +3467,15 @@ function buildSendMessageTools() {
|
|
|
3380
3467
|
},
|
|
3381
3468
|
required: ["query"]
|
|
3382
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
|
+
}
|
|
3383
3479
|
}
|
|
3384
3480
|
];
|
|
3385
3481
|
}
|
|
@@ -3405,13 +3501,6 @@ function parseSearchArguments(json) {
|
|
|
3405
3501
|
}
|
|
3406
3502
|
return null;
|
|
3407
3503
|
}
|
|
3408
|
-
function stripAssistantForHistory(message) {
|
|
3409
|
-
return {
|
|
3410
|
-
role: "assistant",
|
|
3411
|
-
content: message.content,
|
|
3412
|
-
toolCalls: message.toolCalls
|
|
3413
|
-
};
|
|
3414
|
-
}
|
|
3415
3504
|
|
|
3416
3505
|
// src/channel/discord.ts
|
|
3417
3506
|
import {
|
|
@@ -3431,10 +3520,18 @@ var SLEEP_MEMORY_CRON_KEY = "__sleep-memory__";
|
|
|
3431
3520
|
var SLEEP_MEMORY_CRON_PATTERN = "0 * * * *";
|
|
3432
3521
|
var START_CONVERSATION_CRON_KEY = "__start-conversation__";
|
|
3433
3522
|
var START_CONVERSATION_CRON_PATTERN = "*/10 * * * *";
|
|
3434
|
-
var
|
|
3435
|
-
var
|
|
3436
|
-
var
|
|
3437
|
-
var
|
|
3523
|
+
var SCHEDULE_CRON_KEY = "__schedule__";
|
|
3524
|
+
var SCHEDULE_CRON_PATTERN = "0 0 * * *";
|
|
3525
|
+
var SCHEDULE_NOON_CRON_KEY = "__schedule-noon__";
|
|
3526
|
+
var SCHEDULE_NOON_CRON_PATTERN = "0 12 * * *";
|
|
3527
|
+
var DO_ACTIONS = ["generateSchedule", "sleepMemory"];
|
|
3528
|
+
var VIEW_THINGS = [
|
|
3529
|
+
"daily-schedule",
|
|
3530
|
+
"monthly-schedule",
|
|
3531
|
+
"sending-queue",
|
|
3532
|
+
"deferred-queue",
|
|
3533
|
+
"today-availability"
|
|
3534
|
+
];
|
|
3438
3535
|
|
|
3439
3536
|
class BaseChannel {
|
|
3440
3537
|
brain;
|
|
@@ -3454,8 +3551,14 @@ class BaseChannel {
|
|
|
3454
3551
|
static activeChannels = new Map;
|
|
3455
3552
|
constructor(brain) {
|
|
3456
3553
|
this.brain = brain;
|
|
3457
|
-
this.registerCron(SLEEP_MEMORY_CRON_KEY, SLEEP_MEMORY_CRON_PATTERN,
|
|
3458
|
-
|
|
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) {
|
|
3459
3562
|
const availability = await this.brain.getAvailability();
|
|
3460
3563
|
if (availability.status !== "offline") {
|
|
3461
3564
|
logger.debug(`sleepMemory cron: skip — availability=${availability.status}`);
|
|
@@ -3466,12 +3569,9 @@ class BaseChannel {
|
|
|
3466
3569
|
logger.debug(`sleepMemory cron: skip — journal for ${dateKey2} exists`);
|
|
3467
3570
|
return;
|
|
3468
3571
|
}
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
this.registerCron(DAILY_SCHEDULE_CRON_KEY, DAILY_SCHEDULE_CRON_PATTERN, () => this.regenerateSchedules());
|
|
3473
|
-
this.registerCron(DAILY_SCHEDULE_NOON_CRON_KEY, DAILY_SCHEDULE_NOON_CRON_PATTERN, () => this.regenerateSchedules());
|
|
3474
|
-
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);
|
|
3475
3575
|
}
|
|
3476
3576
|
async regenerateSchedules() {
|
|
3477
3577
|
logger.debug(`regenerateSchedules: tick for ${this.brain.brainbase.displayName}`);
|
|
@@ -3693,6 +3793,88 @@ class BaseChannel {
|
|
|
3693
3793
|
static all() {
|
|
3694
3794
|
return Array.from(BaseChannel.activeChannels.values());
|
|
3695
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
|
+
}
|
|
3696
3878
|
static async shutdownAll() {
|
|
3697
3879
|
await Promise.all(BaseChannel.all().map((c) => c.shutdown()));
|
|
3698
3880
|
}
|
|
@@ -4202,6 +4384,63 @@ defineCommand({
|
|
|
4202
4384
|
}
|
|
4203
4385
|
});
|
|
4204
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
|
+
|
|
4205
4444
|
// src/commands/daemon.ts
|
|
4206
4445
|
async function startChannels() {
|
|
4207
4446
|
const items = await brainManager.listAvailableBrain();
|
|
@@ -4238,8 +4477,8 @@ async function startChannels() {
|
|
|
4238
4477
|
async function daemon() {
|
|
4239
4478
|
const logDir = join5(config.brainboxRoot, "logs");
|
|
4240
4479
|
logger.configure({ logDir });
|
|
4241
|
-
configureLlmLog(join5(logDir, "llm"));
|
|
4242
|
-
logger.debug(`daemon: boot (
|
|
4480
|
+
configureLlmLog(config.debug ? join5(logDir, "llm") : undefined);
|
|
4481
|
+
logger.debug(`daemon: boot (debug=${config.debug}, logDir=${logDir}, llmLog=${config.debug})`);
|
|
4243
4482
|
const started = await startChannels();
|
|
4244
4483
|
if (started === 0) {
|
|
4245
4484
|
logger.info("No activated brains with channels. Daemon idling.");
|
|
@@ -4406,6 +4645,33 @@ async function removeBrain(brainId) {
|
|
|
4406
4645
|
}
|
|
4407
4646
|
logger.success(`Removed brain "${brain.displayName}" (${chalk2.cyan(brainId)})`);
|
|
4408
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
|
+
}
|
|
4409
4675
|
function register3(program) {
|
|
4410
4676
|
const cmd = registerCommand(program, {
|
|
4411
4677
|
name: "brain",
|
|
@@ -4416,6 +4682,8 @@ function register3(program) {
|
|
|
4416
4682
|
cmd.command("remove <brainId>").description("Remove a brain and its memory").action(removeBrain);
|
|
4417
4683
|
cmd.command("activate <brainId>").description("Activate a brain").action(activateBrain);
|
|
4418
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);
|
|
4419
4687
|
return cmd;
|
|
4420
4688
|
}
|
|
4421
4689
|
|