wingbot 3.76.3 → 3.76.4-alpha.2
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/package.json +1 -1
- package/src/LLMSession.js +109 -14
package/package.json
CHANGED
package/src/LLMSession.js
CHANGED
|
@@ -170,6 +170,10 @@ const stateData = require('./utils/stateData');
|
|
|
170
170
|
* @prop {LLMCallPreset} [preset]
|
|
171
171
|
*/
|
|
172
172
|
|
|
173
|
+
// max number of consecutive tool-call rounds resolved within a single generate()
|
|
174
|
+
// before we force a tool-less final answer (guards against tool-call loops)
|
|
175
|
+
const MAX_TOOL_CALL_ROUNDS = 5;
|
|
176
|
+
|
|
173
177
|
/**
|
|
174
178
|
* @class LLMSession
|
|
175
179
|
* @implements {PromiseLike<LLMMessage<any>>}
|
|
@@ -224,6 +228,9 @@ class LLMSession {
|
|
|
224
228
|
this._res = res || llm?.res;
|
|
225
229
|
|
|
226
230
|
this._preset = preset;
|
|
231
|
+
|
|
232
|
+
/** @type {PossiblyAsyncContent|null} */
|
|
233
|
+
this._fallbackMessage = null;
|
|
227
234
|
}
|
|
228
235
|
|
|
229
236
|
/**
|
|
@@ -850,6 +857,38 @@ class LLMSession {
|
|
|
850
857
|
return this;
|
|
851
858
|
}
|
|
852
859
|
|
|
860
|
+
/**
|
|
861
|
+
* Sets a message to send to the user when a subsequent `generate()` call
|
|
862
|
+
* fails (e.g. a provider network timeout). Instead of rejecting - which
|
|
863
|
+
* would surface the raw error to the user - the failure is logged and this
|
|
864
|
+
* message is sent in place of the model's reply, so the chain resolves
|
|
865
|
+
* cleanly. If the message resolves to an empty value, the original error
|
|
866
|
+
* is rethrown (same as when no fallback is set).
|
|
867
|
+
*
|
|
868
|
+
* Only affects `generate()`. `generateStructured()` keeps using delegated
|
|
869
|
+
* errors (see {@link onDelegatedError}).
|
|
870
|
+
*
|
|
871
|
+
* @param {PossiblyAsyncContent} content
|
|
872
|
+
* @returns {this}
|
|
873
|
+
*/
|
|
874
|
+
setFallbackMessage (content) {
|
|
875
|
+
this._job(() => {
|
|
876
|
+
this._fallbackMessage = content;
|
|
877
|
+
}, true);
|
|
878
|
+
return this;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* @returns {Promise<string>}
|
|
883
|
+
*/
|
|
884
|
+
async _resolveFallbackContent () {
|
|
885
|
+
const fallback = this._fallbackMessage;
|
|
886
|
+
const content = typeof fallback === 'function'
|
|
887
|
+
? fallback(this._resolveData())
|
|
888
|
+
: fallback;
|
|
889
|
+
return Promise.resolve(content);
|
|
890
|
+
}
|
|
891
|
+
|
|
853
892
|
/**
|
|
854
893
|
*
|
|
855
894
|
* @param {LLMCallPreset} [providerOptions]
|
|
@@ -857,7 +896,28 @@ class LLMSession {
|
|
|
857
896
|
* @returns {this}
|
|
858
897
|
*/
|
|
859
898
|
generate (providerOptions = this._preset, logOptions = {}) {
|
|
860
|
-
this._job(() =>
|
|
899
|
+
this._job(async () => {
|
|
900
|
+
try {
|
|
901
|
+
return await this._generate(providerOptions, logOptions);
|
|
902
|
+
} catch (e) {
|
|
903
|
+
if (this._fallbackMessage === null) {
|
|
904
|
+
throw e;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const content = await this._resolveFallbackContent();
|
|
908
|
+
if (!content) {
|
|
909
|
+
// no usable fallback message - propagate the original error
|
|
910
|
+
throw e;
|
|
911
|
+
}
|
|
912
|
+
this._llm.log.error(`LLMSession.generate failed, sending fallback message: ${e.message}`, e);
|
|
913
|
+
|
|
914
|
+
/** @type {LLMMessage} */
|
|
915
|
+
const result = { role: ROLE_ASSISTANT, content };
|
|
916
|
+
this._generatedIndex = this._chat.length;
|
|
917
|
+
this._chat.push(result);
|
|
918
|
+
return result;
|
|
919
|
+
}
|
|
920
|
+
});
|
|
861
921
|
return this;
|
|
862
922
|
}
|
|
863
923
|
|
|
@@ -914,7 +974,11 @@ class LLMSession {
|
|
|
914
974
|
async _generate (providerOptions = this._preset, logOptions = {}) {
|
|
915
975
|
let result = await this._llm.generate(this, providerOptions, logOptions);
|
|
916
976
|
|
|
917
|
-
|
|
977
|
+
// the model may chain several rounds of tool calls before it produces
|
|
978
|
+
// a final text answer - keep resolving them until it stops (bounded)
|
|
979
|
+
let rounds = 0;
|
|
980
|
+
while (result.toolCalls?.length && rounds < MAX_TOOL_CALL_ROUNDS) {
|
|
981
|
+
rounds += 1;
|
|
918
982
|
const toolCalls = [];
|
|
919
983
|
const results = await Promise.all(
|
|
920
984
|
result.toolCalls.map(async (tc) => {
|
|
@@ -936,27 +1000,54 @@ class LLMSession {
|
|
|
936
1000
|
);
|
|
937
1001
|
result = await this._llm.generate(this, providerOptions, logOptions);
|
|
938
1002
|
} else {
|
|
939
|
-
// everything failed
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
...providerOptions,
|
|
948
|
-
toolChoice: 'none'
|
|
949
|
-
};
|
|
950
|
-
result = await this._llm.generate(this, overrideChoice, logOptions);
|
|
1003
|
+
// everything failed - force a final text answer without tools
|
|
1004
|
+
this._llm.log.error(
|
|
1005
|
+
`LLMSession: all ${result.toolCalls.length} tool call(s) failed in round ${rounds}, `
|
|
1006
|
+
+ 'forcing a tool-less final answer',
|
|
1007
|
+
{ toolCalls: result.toolCalls }
|
|
1008
|
+
);
|
|
1009
|
+
result = await this._generateWithoutTools(providerOptions, logOptions);
|
|
1010
|
+
break;
|
|
951
1011
|
}
|
|
952
1012
|
}
|
|
953
1013
|
|
|
1014
|
+
// safety net: if the model is still requesting tools (e.g. it hit the
|
|
1015
|
+
// round limit), force one final tool-less generation so we never return
|
|
1016
|
+
// a tool-call message (content === null) to the send pipeline
|
|
1017
|
+
if (result.toolCalls?.length) {
|
|
1018
|
+
this._llm.log.error(
|
|
1019
|
+
`LLMSession: reached MAX_TOOL_CALL_ROUNDS (${MAX_TOOL_CALL_ROUNDS}), `
|
|
1020
|
+
+ 'dropping pending tool calls and forcing a tool-less final answer',
|
|
1021
|
+
{ toolCalls: result.toolCalls }
|
|
1022
|
+
);
|
|
1023
|
+
result = await this._generateWithoutTools(providerOptions, logOptions);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
954
1026
|
this._generatedIndex = this._chat.length;
|
|
955
1027
|
this._chat.push(result);
|
|
956
1028
|
|
|
957
1029
|
return result;
|
|
958
1030
|
}
|
|
959
1031
|
|
|
1032
|
+
/**
|
|
1033
|
+
*
|
|
1034
|
+
* @param {LLMCallPreset} providerOptions
|
|
1035
|
+
* @param {LLMLogOptions} logOptions
|
|
1036
|
+
* @returns {Promise<LLMMessage<any>>}
|
|
1037
|
+
*/
|
|
1038
|
+
_generateWithoutTools (providerOptions, logOptions) {
|
|
1039
|
+
const overrideChoice = typeof providerOptions === 'string'
|
|
1040
|
+
? {
|
|
1041
|
+
preset: providerOptions,
|
|
1042
|
+
toolChoice: 'none'
|
|
1043
|
+
}
|
|
1044
|
+
: {
|
|
1045
|
+
...providerOptions,
|
|
1046
|
+
toolChoice: 'none'
|
|
1047
|
+
};
|
|
1048
|
+
return this._llm.generate(this, overrideChoice, logOptions);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
960
1051
|
/**
|
|
961
1052
|
*
|
|
962
1053
|
* @param {ToolCall} toolCall
|
|
@@ -1089,6 +1180,10 @@ class LLMSession {
|
|
|
1089
1180
|
* @returns {LLMMessage[]}
|
|
1090
1181
|
*/
|
|
1091
1182
|
static toMessages (result) {
|
|
1183
|
+
// tool-call / structured messages carry no text content - nothing to send
|
|
1184
|
+
if (typeof result.content !== 'string') {
|
|
1185
|
+
return [];
|
|
1186
|
+
}
|
|
1092
1187
|
let filtered = result.content
|
|
1093
1188
|
.replace(/\n\n\n+/g, '\n\n')
|
|
1094
1189
|
.split(/\n\n+(?!\s*-)/g)
|