neoagent 3.2.1-beta.7 → 3.2.1-beta.8
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 +2 -2
- package/server/http/routes.js +7 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +5 -5
- package/server/services/ai/loop/completion_judge.js +16 -7
- package/server/services/ai/loop/conversation_loop.js +122 -129
- package/server/services/ai/model_failure_cache.js +47 -8
- package/server/services/ai/providers/google.js +101 -95
- package/server/services/ai/terminal_reply.js +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neoagent",
|
|
3
|
-
"version": "3.2.1-beta.
|
|
3
|
+
"version": "3.2.1-beta.8",
|
|
4
4
|
"description": "Self-hosted AI agent for long-running tasks, automation, messaging, device control, and local memory",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"main": "server/index.js",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
70
70
|
"@bitwarden/cli": "^2026.6.0",
|
|
71
|
-
"@google/
|
|
71
|
+
"@google/genai": "^2.13.0",
|
|
72
72
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
73
73
|
"@remotion/cli": "^4.0.459",
|
|
74
74
|
"@slidev/cli": "^52.18.0",
|
package/server/http/routes.js
CHANGED
|
@@ -56,6 +56,13 @@ function registerApiRoutes(app) {
|
|
|
56
56
|
if (isBillingEnabled()) {
|
|
57
57
|
app.use('/api/billing/webhook', require('../routes/billing_webhook'));
|
|
58
58
|
app.use('/api/billing', require('../routes/billing'));
|
|
59
|
+
} else {
|
|
60
|
+
// The Flutter client probes this public endpoint to decide whether to show
|
|
61
|
+
// billing. Return an explicit disabled state instead of generating a noisy
|
|
62
|
+
// 404 on every client startup.
|
|
63
|
+
app.get('/api/billing/plans', (_req, res) => {
|
|
64
|
+
res.json({ enabled: false, plans: null });
|
|
65
|
+
});
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
setupTelnyxWebhook(app);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
38fc05bea2f5bf14dda4c0f58b31ff9a
|
|
@@ -33,10 +33,10 @@ addEventListener("message", eventListener);
|
|
|
33
33
|
if (!window._flutter) {
|
|
34
34
|
window._flutter = {};
|
|
35
35
|
}
|
|
36
|
-
_flutter.buildConfig = {"engineRevision":"
|
|
36
|
+
_flutter.buildConfig = {"engineRevision":"0cd610717bde95fd88343c64f81c11ba4e5c0010","builds":[{"compileTarget":"dart2js","renderer":"canvaskit","mainJsPath":"main.dart.js"},{}]};
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "222530411" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|
|
@@ -44367,7 +44367,7 @@ $S:4}
|
|
|
44367
44367
|
A.bnI.prototype={
|
|
44368
44368
|
$1(a){var s=A.fp().b
|
|
44369
44369
|
s=s==null?null:s.canvasKitBaseUrl
|
|
44370
|
-
return(s==null?"https://www.gstatic.com/flutter-canvaskit/
|
|
44370
|
+
return(s==null?"https://www.gstatic.com/flutter-canvaskit/0cd610717bde95fd88343c64f81c11ba4e5c0010/":s)+a},
|
|
44371
44371
|
$S:42}
|
|
44372
44372
|
A.a2F.prototype={
|
|
44373
44373
|
gF(a){var s=this.a
|
|
@@ -144506,7 +144506,7 @@ if(r){r=s.d
|
|
|
144506
144506
|
r===$&&A.b()
|
|
144507
144507
|
p.push(A.ju(q,A.jx(!1,new A.Y(B.wP,A.cU(new A.cH(B.kE,new A.acU(r,q),q),q,q),q),!1,B.J,!0),q,q,0,0,0,q))}if(!s.ay){r=s.e
|
|
144508
144508
|
r===$&&A.b()
|
|
144509
|
-
r=B.b.t("
|
|
144509
|
+
r=B.b.t("mryrwx0x-27df61b").length!==0&&r.b}else r=!1
|
|
144510
144510
|
if(r){r=s.d
|
|
144511
144511
|
r===$&&A.b()
|
|
144512
144512
|
r=r.aU&&!r.ag?84:0
|
|
@@ -150393,7 +150393,7 @@ $S:0}
|
|
|
150393
150393
|
A.a3j.prototype={}
|
|
150394
150394
|
A.WC.prototype={
|
|
150395
150395
|
r9(a){var s=this
|
|
150396
|
-
if(B.b.t("
|
|
150396
|
+
if(B.b.t("mryrwx0x-27df61b").length===0||s.a!=null)return
|
|
150397
150397
|
s.Co()
|
|
150398
150398
|
s.a=A.mL(B.Yh,new A.bmK(s))},
|
|
150399
150399
|
Co(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
|
|
@@ -150411,7 +150411,7 @@ if(!t.f.b(k)){s=1
|
|
|
150411
150411
|
break}i=J.a0(k,"buildId")
|
|
150412
150412
|
h=i==null?null:B.b.t(J.q(i))
|
|
150413
150413
|
j=h==null?"":h
|
|
150414
|
-
if(J.bv(j)===0||J.e(j,"
|
|
150414
|
+
if(J.bv(j)===0||J.e(j,"mryrwx0x-27df61b")){s=1
|
|
150415
150415
|
break}n.b=!0
|
|
150416
150416
|
n.H()
|
|
150417
150417
|
p=2
|
|
@@ -150428,7 +150428,7 @@ case 2:return A.i(o.at(-1),r)}})
|
|
|
150428
150428
|
return A.k($async$Co,r)},
|
|
150429
150429
|
wV(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1
|
|
150430
150430
|
var $async$wV=A.h(function(a2,a3){if(a2===1){o.push(a3)
|
|
150431
|
-
s=p}for(;;)switch(s){case 0:if(B.b.t("
|
|
150431
|
+
s=p}for(;;)switch(s){case 0:if(B.b.t("mryrwx0x-27df61b").length===0||n.c){s=1
|
|
150432
150432
|
break}n.c=!0
|
|
150433
150433
|
n.H()
|
|
150434
150434
|
p=4
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const { normalizeCompletionConfidence } = require('../completion');
|
|
4
4
|
const { normalizeOutgoingMessage } = require('../messagingFallback');
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
isDeferredWorkReply,
|
|
7
|
+
isTerminalQuestionOrBlockerReply,
|
|
8
|
+
} = require('../terminal_reply');
|
|
6
9
|
const {
|
|
7
10
|
summarizeAvailableTools,
|
|
8
11
|
summarizeToolExecutions,
|
|
@@ -243,13 +246,19 @@ function normalizeCompletionDecision(raw, fallbackStatus = 'continue') {
|
|
|
243
246
|
}
|
|
244
247
|
|
|
245
248
|
function enforceTerminalReplyDecision(decision, lastReply) {
|
|
246
|
-
if (
|
|
247
|
-
return
|
|
249
|
+
if (isDeferredWorkReply(lastReply)) {
|
|
250
|
+
return {
|
|
251
|
+
status: 'continue',
|
|
252
|
+
reason: 'The latest reply only announces or promises unfinished work; the run must continue or return a concrete blocker.',
|
|
253
|
+
};
|
|
248
254
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
255
|
+
if (decision?.status === 'continue' && isTerminalQuestionOrBlockerReply(lastReply)) {
|
|
256
|
+
return {
|
|
257
|
+
status: 'blocked',
|
|
258
|
+
reason: 'The latest reply asks for user input or states a concrete blocker, so the run must wait instead of repeating it.',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return decision;
|
|
253
262
|
}
|
|
254
263
|
|
|
255
264
|
// Intentionally lightweight (200-token cap, self-contained) so the model can
|
|
@@ -422,13 +422,20 @@ async function getFailureFallbackModelId(
|
|
|
422
422
|
preferredFallbackId = null,
|
|
423
423
|
failureError = null,
|
|
424
424
|
signal = null,
|
|
425
|
+
excludedModelIds = [],
|
|
425
426
|
) {
|
|
426
427
|
const { getSupportedModels } = require('../models');
|
|
427
428
|
const aiSettings = getAiSettings(userId, agentId);
|
|
428
429
|
const models = await getSupportedModels(userId, agentId, { signal });
|
|
430
|
+
const excluded = new Set(
|
|
431
|
+
[...excludedModelIds]
|
|
432
|
+
.map((id) => String(id || '').trim())
|
|
433
|
+
.filter(Boolean),
|
|
434
|
+
);
|
|
429
435
|
const availableModels = models.filter(
|
|
430
436
|
(model) => model.available !== false
|
|
431
|
-
&& !isModelCoolingDown(userId, agentId, model.id)
|
|
437
|
+
&& !isModelCoolingDown(userId, agentId, model.id)
|
|
438
|
+
&& !excluded.has(model.id),
|
|
432
439
|
);
|
|
433
440
|
const configuredEnabledIds = Array.isArray(aiSettings.enabled_models)
|
|
434
441
|
? aiSettings.enabled_models.map((id) => String(id).trim()).filter(Boolean)
|
|
@@ -438,15 +445,26 @@ async function getFailureFallbackModelId(
|
|
|
438
445
|
? availableModels.filter((model) => enabledIds.includes(model.id))
|
|
439
446
|
: availableModels;
|
|
440
447
|
const fallbackSearchPool = pool;
|
|
441
|
-
const currentModel = resolveModelSelection(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
|
|
448
|
+
const currentModel = resolveModelSelection(models, currentModelId);
|
|
449
|
+
|
|
450
|
+
// Provider-wide failures (credentials, throttling, or service outages) are
|
|
451
|
+
// likely to affect another model on the same provider. Prefer a different
|
|
452
|
+
// provider before consulting the configured fallback.
|
|
453
|
+
const failureStatus = Number(
|
|
454
|
+
failureError?.status
|
|
455
|
+
?? failureError?.statusCode
|
|
456
|
+
?? failureError?.response?.status,
|
|
457
|
+
);
|
|
458
|
+
const isProviderScopedFailure = (
|
|
459
|
+
failureStatus === 401
|
|
460
|
+
|| failureStatus === 403
|
|
461
|
+
|| failureStatus === 429
|
|
462
|
+
|| (failureStatus >= 500 && failureStatus < 600)
|
|
463
|
+
|| /rate.?limit|free-models-per|service unavailable|provider unavailable|authentication|api key/i
|
|
464
|
+
.test(String(failureError?.message || ''))
|
|
465
|
+
);
|
|
448
466
|
|
|
449
|
-
if (preferredFallbackId && !
|
|
467
|
+
if (preferredFallbackId && !isProviderScopedFailure) {
|
|
450
468
|
const preferred = resolveModelSelection(fallbackSearchPool, preferredFallbackId)
|
|
451
469
|
|| resolveModelSelection(availableModels, preferredFallbackId);
|
|
452
470
|
if (preferred && preferred.id !== currentModel?.id) return preferred.id;
|
|
@@ -458,8 +476,7 @@ async function getFailureFallbackModelId(
|
|
|
458
476
|
if (differentProvider) return differentProvider.id;
|
|
459
477
|
}
|
|
460
478
|
|
|
461
|
-
// If no different-provider model exists, still try the preferred fallback
|
|
462
|
-
// even on rate limits (it's better than nothing).
|
|
479
|
+
// If no different-provider model exists, still try the preferred fallback.
|
|
463
480
|
if (preferredFallbackId) {
|
|
464
481
|
const preferred = resolveModelSelection(fallbackSearchPool, preferredFallbackId)
|
|
465
482
|
|| resolveModelSelection(availableModels, preferredFallbackId);
|
|
@@ -491,6 +508,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
491
508
|
let model = null;
|
|
492
509
|
let modelSelectionId = null;
|
|
493
510
|
let providerName = null;
|
|
511
|
+
const modelTurnFailedModelSelectionIds = new Set();
|
|
494
512
|
let messages = [];
|
|
495
513
|
let iteration = 0;
|
|
496
514
|
let totalTokens = 0;
|
|
@@ -606,8 +624,17 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
606
624
|
}
|
|
607
625
|
db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
608
626
|
.run(modelSelectionId, runId);
|
|
609
|
-
const
|
|
627
|
+
const recordFailedModel = (failedSelectionId, error, excludedModels) => {
|
|
628
|
+
excludedModels.add(failedSelectionId);
|
|
610
629
|
recordModelFailure(userId, agentId, failedSelectionId, error);
|
|
630
|
+
};
|
|
631
|
+
const switchToFallbackModel = async (
|
|
632
|
+
failedSelectionId,
|
|
633
|
+
error,
|
|
634
|
+
phase,
|
|
635
|
+
excludedModels,
|
|
636
|
+
) => {
|
|
637
|
+
recordFailedModel(failedSelectionId, error, excludedModels);
|
|
611
638
|
const fallbackModelId = await getFailureFallbackModelId(
|
|
612
639
|
userId,
|
|
613
640
|
agentId,
|
|
@@ -615,9 +642,21 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
615
642
|
aiSettings.fallback_model_id,
|
|
616
643
|
error,
|
|
617
644
|
engine.getRunMeta(runId)?.abortController?.signal,
|
|
645
|
+
excludedModels,
|
|
618
646
|
);
|
|
619
647
|
if (!fallbackModelId || fallbackModelId === failedSelectionId) return false;
|
|
620
|
-
|
|
648
|
+
const failureSummary = summarizeForLog(error?.message || error, 180);
|
|
649
|
+
console.log(
|
|
650
|
+
`[Engine] ${phase} failed on ${failedSelectionId}: ${failureSummary}; attempting fallback to: ${fallbackModelId}`
|
|
651
|
+
);
|
|
652
|
+
engine.recordRunEvent(userId, runId, 'model_fallback', {
|
|
653
|
+
phase,
|
|
654
|
+
failedModel: failedSelectionId,
|
|
655
|
+
fallbackModel: fallbackModelId,
|
|
656
|
+
errorCode: error?.code || null,
|
|
657
|
+
errorStatus: error?.status || error?.statusCode || error?.response?.status || null,
|
|
658
|
+
error: failureSummary,
|
|
659
|
+
}, { agentId });
|
|
621
660
|
engine.emit(userId, 'run:interim', {
|
|
622
661
|
runId,
|
|
623
662
|
message: `Model service failed on ${failedSelectionId}; retrying with ${fallbackModelId}.`,
|
|
@@ -647,17 +686,28 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
647
686
|
return true;
|
|
648
687
|
};
|
|
649
688
|
const runWithModelFallback = async (phase, fn) => {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
689
|
+
let recoveries = 0;
|
|
690
|
+
const failedModels = new Set();
|
|
691
|
+
while (true) {
|
|
692
|
+
try {
|
|
693
|
+
const result = await fn();
|
|
694
|
+
recordModelSuccess(userId, agentId, modelSelectionId);
|
|
695
|
+
return result;
|
|
696
|
+
} catch (err) {
|
|
697
|
+
if (recoveries >= loopPolicy.maxModelFailureRecoveries) {
|
|
698
|
+
recordFailedModel(modelSelectionId, err, failedModels);
|
|
699
|
+
throw err;
|
|
700
|
+
}
|
|
701
|
+
const failedSelectionId = modelSelectionId;
|
|
702
|
+
const switched = await switchToFallbackModel(
|
|
703
|
+
failedSelectionId,
|
|
704
|
+
err,
|
|
705
|
+
phase,
|
|
706
|
+
failedModels,
|
|
707
|
+
);
|
|
708
|
+
if (!switched) throw err;
|
|
709
|
+
recoveries += 1;
|
|
710
|
+
}
|
|
661
711
|
}
|
|
662
712
|
};
|
|
663
713
|
|
|
@@ -1483,102 +1533,69 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1483
1533
|
let responseModel = model;
|
|
1484
1534
|
let streamContent = '';
|
|
1485
1535
|
|
|
1486
|
-
const tryModelCall = async (
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
messages,
|
|
1493
|
-
tools,
|
|
1494
|
-
options: {
|
|
1495
|
-
...options,
|
|
1496
|
-
userId,
|
|
1497
|
-
agentId,
|
|
1498
|
-
runId,
|
|
1499
|
-
phase: 'model_turn',
|
|
1500
|
-
signal: engine.getRunMeta(runId)?.abortController?.signal,
|
|
1501
|
-
},
|
|
1502
|
-
runId,
|
|
1503
|
-
iteration,
|
|
1504
|
-
});
|
|
1505
|
-
response = modelCall.response;
|
|
1506
|
-
responseModel = modelCall.responseModel;
|
|
1507
|
-
streamContent = modelCall.streamContent;
|
|
1508
|
-
recordModelSuccess(userId, agentId, modelSelectionId);
|
|
1509
|
-
} catch (err) {
|
|
1510
|
-
console.error(`[Engine] Model call failed (${model}):`, err.message);
|
|
1511
|
-
recordModelFailure(userId, agentId, modelSelectionId, err);
|
|
1512
|
-
const fallbackModelId = retryForFallback
|
|
1513
|
-
? await getFailureFallbackModelId(
|
|
1514
|
-
userId,
|
|
1515
|
-
agentId,
|
|
1516
|
-
modelSelectionId,
|
|
1517
|
-
aiSettings.fallback_model_id,
|
|
1518
|
-
err,
|
|
1519
|
-
engine.getRunMeta(runId)?.abortController?.signal,
|
|
1520
|
-
)
|
|
1521
|
-
: null;
|
|
1522
|
-
if (fallbackModelId) {
|
|
1523
|
-
const failedModel = model;
|
|
1524
|
-
console.log(`[Engine] Attempting fallback to: ${fallbackModelId}`);
|
|
1525
|
-
const fallback = await getProviderForUser(
|
|
1526
|
-
userId,
|
|
1527
|
-
userMessage,
|
|
1528
|
-
triggerType === 'subagent',
|
|
1529
|
-
fallbackModelId,
|
|
1530
|
-
{
|
|
1531
|
-
...providerStatusConfig,
|
|
1532
|
-
signal: engine.getRunMeta(runId)?.abortController?.signal,
|
|
1533
|
-
}
|
|
1534
|
-
);
|
|
1535
|
-
provider = fallback.provider;
|
|
1536
|
-
model = fallback.model;
|
|
1537
|
-
modelSelectionId = fallback.modelSelectionId;
|
|
1538
|
-
providerName = fallback.providerName;
|
|
1539
|
-
db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
1540
|
-
.run(modelSelectionId, runId);
|
|
1541
|
-
Object.assign(engine.getRunMeta(runId) || {}, {
|
|
1542
|
-
model,
|
|
1543
|
-
modelSelectionId,
|
|
1544
|
-
providerName,
|
|
1545
|
-
});
|
|
1546
|
-
|
|
1547
|
-
const retryMessages = sanitizeConversationMessages([
|
|
1548
|
-
...messages,
|
|
1549
|
-
{
|
|
1550
|
-
role: 'system',
|
|
1551
|
-
content: buildModelFailureLoopPrompt({
|
|
1552
|
-
failedModel,
|
|
1553
|
-
nextModel: model,
|
|
1554
|
-
errorMessage: err.message
|
|
1555
|
-
})
|
|
1556
|
-
}
|
|
1557
|
-
]);
|
|
1558
|
-
|
|
1559
|
-
const fallbackCall = await engine.requestModelResponse({
|
|
1536
|
+
const tryModelCall = async () => {
|
|
1537
|
+
let requestMessages = messages;
|
|
1538
|
+
let requestPhase = 'model_turn';
|
|
1539
|
+
while (true) {
|
|
1540
|
+
try {
|
|
1541
|
+
const modelCall = await engine.requestModelResponse({
|
|
1560
1542
|
provider,
|
|
1561
1543
|
providerName,
|
|
1562
1544
|
model,
|
|
1563
|
-
messages:
|
|
1545
|
+
messages: requestMessages,
|
|
1564
1546
|
tools,
|
|
1565
1547
|
options: {
|
|
1566
1548
|
...options,
|
|
1567
1549
|
userId,
|
|
1568
1550
|
agentId,
|
|
1569
1551
|
runId,
|
|
1570
|
-
phase:
|
|
1552
|
+
phase: requestPhase,
|
|
1571
1553
|
signal: engine.getRunMeta(runId)?.abortController?.signal,
|
|
1572
1554
|
},
|
|
1573
1555
|
runId,
|
|
1574
1556
|
iteration,
|
|
1575
1557
|
});
|
|
1576
|
-
response =
|
|
1577
|
-
responseModel =
|
|
1578
|
-
streamContent =
|
|
1558
|
+
response = modelCall.response;
|
|
1559
|
+
responseModel = modelCall.responseModel;
|
|
1560
|
+
streamContent = modelCall.streamContent;
|
|
1579
1561
|
recordModelSuccess(userId, agentId, modelSelectionId);
|
|
1580
|
-
|
|
1581
|
-
|
|
1562
|
+
return;
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
console.error(`[Engine] Model call failed (${model}):`, err.message);
|
|
1565
|
+
const runSignal = engine.getRunMeta(runId)?.abortController?.signal;
|
|
1566
|
+
if (isAbortError(err) || runSignal?.aborted) throw err;
|
|
1567
|
+
if (modelFailureRecoveries >= loopPolicy.maxModelFailureRecoveries) {
|
|
1568
|
+
recordFailedModel(
|
|
1569
|
+
modelSelectionId,
|
|
1570
|
+
err,
|
|
1571
|
+
modelTurnFailedModelSelectionIds,
|
|
1572
|
+
);
|
|
1573
|
+
throw err;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
const failedModel = model;
|
|
1577
|
+
const switched = await switchToFallbackModel(
|
|
1578
|
+
modelSelectionId,
|
|
1579
|
+
err,
|
|
1580
|
+
'model turn',
|
|
1581
|
+
modelTurnFailedModelSelectionIds,
|
|
1582
|
+
);
|
|
1583
|
+
if (!switched) throw err;
|
|
1584
|
+
|
|
1585
|
+
modelFailureRecoveries += 1;
|
|
1586
|
+
failedStepCount += 1;
|
|
1587
|
+
requestPhase = 'model_turn_fallback';
|
|
1588
|
+
requestMessages = sanitizeConversationMessages([
|
|
1589
|
+
...messages,
|
|
1590
|
+
{
|
|
1591
|
+
role: 'system',
|
|
1592
|
+
content: buildModelFailureLoopPrompt({
|
|
1593
|
+
failedModel,
|
|
1594
|
+
nextModel: model,
|
|
1595
|
+
errorMessage: err.message
|
|
1596
|
+
})
|
|
1597
|
+
}
|
|
1598
|
+
]);
|
|
1582
1599
|
}
|
|
1583
1600
|
}
|
|
1584
1601
|
};
|
|
@@ -1601,30 +1618,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1601
1618
|
continue;
|
|
1602
1619
|
}
|
|
1603
1620
|
if (lifecycleControl?.action === 'stop' || lifecycleControl?.action === 'interrupt') break;
|
|
1604
|
-
const modelError = String(err?.message || 'Model call failed');
|
|
1605
|
-
|
|
1606
|
-
if (modelFailureRecoveries < loopPolicy.maxModelFailureRecoveries) {
|
|
1607
|
-
const failedModel = model;
|
|
1608
|
-
const switched = await switchToFallbackModel(modelSelectionId, err, 'model turn');
|
|
1609
|
-
if (!switched) throw err;
|
|
1610
|
-
modelFailureRecoveries += 1;
|
|
1611
|
-
failedStepCount += 1;
|
|
1612
|
-
messages.push({
|
|
1613
|
-
role: 'system',
|
|
1614
|
-
content: buildModelFailureLoopPrompt({
|
|
1615
|
-
failedModel,
|
|
1616
|
-
nextModel: model,
|
|
1617
|
-
errorMessage: modelError
|
|
1618
|
-
})
|
|
1619
|
-
});
|
|
1620
|
-
engine.emit(userId, 'run:interim', {
|
|
1621
|
-
runId,
|
|
1622
|
-
message: 'Model call failed; adapting and retrying autonomously.',
|
|
1623
|
-
phase: 'recovering'
|
|
1624
|
-
});
|
|
1625
|
-
continue;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
1621
|
throw err;
|
|
1629
1622
|
}
|
|
1630
1623
|
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { isAbortError } = require('../../utils/abort');
|
|
4
|
+
const {
|
|
5
|
+
getErrorCode,
|
|
6
|
+
getHttpStatus,
|
|
7
|
+
isTransientIoError,
|
|
8
|
+
retryAfterMilliseconds,
|
|
9
|
+
} = require('../../utils/retry');
|
|
10
|
+
|
|
3
11
|
const DEFAULT_COOLDOWN_MS = 15 * 60 * 1000;
|
|
12
|
+
const DEFAULT_RECOVERY_COOLDOWN_MS = 60 * 1000;
|
|
4
13
|
const failures = new Map();
|
|
5
14
|
|
|
6
15
|
function cacheKey(userId, agentId, modelSelectionId) {
|
|
@@ -11,28 +20,57 @@ function cacheKey(userId, agentId, modelSelectionId) {
|
|
|
11
20
|
].join(':');
|
|
12
21
|
}
|
|
13
22
|
|
|
14
|
-
function
|
|
23
|
+
function readPermanentCooldownMs() {
|
|
15
24
|
const configured = Number(process.env.NEOAGENT_MODEL_NOT_FOUND_COOLDOWN_MS);
|
|
16
25
|
if (!Number.isFinite(configured)) return DEFAULT_COOLDOWN_MS;
|
|
17
26
|
return Math.max(1_000, Math.min(configured, 24 * 60 * 60 * 1000));
|
|
18
27
|
}
|
|
19
28
|
|
|
29
|
+
function readRecoveryCooldownMs() {
|
|
30
|
+
const configured = Number(process.env.NEOAGENT_MODEL_RECOVERY_COOLDOWN_MS);
|
|
31
|
+
if (!Number.isFinite(configured)) return DEFAULT_RECOVERY_COOLDOWN_MS;
|
|
32
|
+
return Math.max(1_000, Math.min(configured, 15 * 60 * 1000));
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
function isPermanentModelFailure(error) {
|
|
21
|
-
const status =
|
|
22
|
-
error?.status
|
|
23
|
-
?? error?.statusCode
|
|
24
|
-
?? error?.response?.status,
|
|
25
|
-
);
|
|
36
|
+
const status = getHttpStatus(error);
|
|
26
37
|
if (status === 404) return true;
|
|
27
38
|
return /\b404\b.*\b(model|nim|provider|request)\b|\b(model|nim)\b.*\b404\b/i
|
|
28
39
|
.test(String(error?.message || ''));
|
|
29
40
|
}
|
|
30
41
|
|
|
42
|
+
function isRecoverableModelFailure(error) {
|
|
43
|
+
if (!error || isAbortError(error) || isPermanentModelFailure(error)) return false;
|
|
44
|
+
const status = getHttpStatus(error);
|
|
45
|
+
if (status === 401 || status === 403 || status === 429 || (status >= 500 && status < 600)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
const code = String(getErrorCode(error) || '');
|
|
49
|
+
if (code === 'MODEL_EMPTY_RESPONSE' || code === 'MODEL_CALL_TIMEOUT') return true;
|
|
50
|
+
if (isTransientIoError(error)) return true;
|
|
51
|
+
return /\bempty response|temporarily unavailable|overloaded|rate.?limit|timed? ?out|network error\b/i
|
|
52
|
+
.test(String(error?.message || ''));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveCooldownMs(error, now) {
|
|
56
|
+
if (isPermanentModelFailure(error)) return readPermanentCooldownMs();
|
|
57
|
+
if (!isRecoverableModelFailure(error)) return 0;
|
|
58
|
+
|
|
59
|
+
const configured = readRecoveryCooldownMs();
|
|
60
|
+
const retryAfter = retryAfterMilliseconds(
|
|
61
|
+
error?.headers || error?.response?.headers,
|
|
62
|
+
now,
|
|
63
|
+
);
|
|
64
|
+
if (!Number.isFinite(retryAfter)) return configured;
|
|
65
|
+
return Math.max(configured, Math.min(retryAfter, 15 * 60 * 1000));
|
|
66
|
+
}
|
|
67
|
+
|
|
31
68
|
function recordModelFailure(userId, agentId, modelSelectionId, error, now = Date.now()) {
|
|
32
69
|
const modelId = String(modelSelectionId || '').trim();
|
|
33
|
-
|
|
70
|
+
const cooldownMs = resolveCooldownMs(error, now);
|
|
71
|
+
if (!modelId || cooldownMs <= 0) return false;
|
|
34
72
|
failures.set(cacheKey(userId, agentId, modelId), {
|
|
35
|
-
expiresAt: now +
|
|
73
|
+
expiresAt: now + cooldownMs,
|
|
36
74
|
});
|
|
37
75
|
return true;
|
|
38
76
|
}
|
|
@@ -64,6 +102,7 @@ module.exports = {
|
|
|
64
102
|
clearModelFailureCache,
|
|
65
103
|
isModelCoolingDown,
|
|
66
104
|
isPermanentModelFailure,
|
|
105
|
+
isRecoverableModelFailure,
|
|
67
106
|
recordModelFailure,
|
|
68
107
|
recordModelSuccess,
|
|
69
108
|
};
|
|
@@ -1,7 +1,63 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { GoogleGenAI } = require('@google/genai');
|
|
2
4
|
const { BaseProvider } = require('./base');
|
|
3
5
|
const { fetchResponseText } = require('../../network/http');
|
|
4
6
|
|
|
7
|
+
function parseToolArguments(value) {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(value || '{}');
|
|
10
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
11
|
+
? parsed
|
|
12
|
+
: {};
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeUsage(usage) {
|
|
19
|
+
if (!usage) return null;
|
|
20
|
+
return {
|
|
21
|
+
inputTokens: usage.promptTokenCount || 0,
|
|
22
|
+
outputTokens: usage.candidatesTokenCount || 0,
|
|
23
|
+
reasoningTokens: usage.thoughtsTokenCount || 0,
|
|
24
|
+
cachedReadTokens: usage.cachedContentTokenCount || 0,
|
|
25
|
+
cacheWriteTokens: 0,
|
|
26
|
+
promptTokens: usage.promptTokenCount || 0,
|
|
27
|
+
completionTokens: usage.candidatesTokenCount || 0,
|
|
28
|
+
totalTokens: usage.totalTokenCount || 0
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function collectResponseParts(response, toolCalls, seenToolCalls = null) {
|
|
33
|
+
let content = '';
|
|
34
|
+
for (const candidate of response?.candidates || []) {
|
|
35
|
+
for (const part of candidate.content?.parts || []) {
|
|
36
|
+
if (part.text && part.thought !== true) content += part.text;
|
|
37
|
+
if (!part.functionCall?.name) continue;
|
|
38
|
+
|
|
39
|
+
const args = part.functionCall.args || {};
|
|
40
|
+
const providerCallId = String(part.functionCall.id || '').trim();
|
|
41
|
+
const signature = providerCallId
|
|
42
|
+
|| `${part.functionCall.name}:${JSON.stringify(args)}`;
|
|
43
|
+
if (seenToolCalls?.has(signature)) continue;
|
|
44
|
+
seenToolCalls?.add(signature);
|
|
45
|
+
|
|
46
|
+
toolCalls.push({
|
|
47
|
+
id: providerCallId
|
|
48
|
+
|| `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
49
|
+
type: 'function',
|
|
50
|
+
function: {
|
|
51
|
+
name: part.functionCall.name,
|
|
52
|
+
arguments: JSON.stringify(args),
|
|
53
|
+
thought_signature: part.thoughtSignature
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return content;
|
|
59
|
+
}
|
|
60
|
+
|
|
5
61
|
class GoogleProvider extends BaseProvider {
|
|
6
62
|
constructor(config = {}) {
|
|
7
63
|
super(config);
|
|
@@ -29,7 +85,7 @@ class GoogleProvider extends BaseProvider {
|
|
|
29
85
|
'gemini-1.5-flash': 1048576,
|
|
30
86
|
};
|
|
31
87
|
this.apiKey = config.apiKey || process.env.GOOGLE_AI_KEY;
|
|
32
|
-
this.genAI = new
|
|
88
|
+
this.genAI = new GoogleGenAI({ apiKey: this.apiKey });
|
|
33
89
|
}
|
|
34
90
|
|
|
35
91
|
async listModels(signal = null) {
|
|
@@ -78,11 +134,23 @@ class GoogleProvider extends BaseProvider {
|
|
|
78
134
|
functionDeclarations: tools.map(tool => ({
|
|
79
135
|
name: tool.name,
|
|
80
136
|
description: tool.description,
|
|
81
|
-
|
|
137
|
+
parametersJsonSchema: tool.parameters || { type: 'object', properties: {} }
|
|
82
138
|
}))
|
|
83
139
|
}];
|
|
84
140
|
}
|
|
85
141
|
|
|
142
|
+
buildGenerateConfig(systemInstruction, tools, options) {
|
|
143
|
+
const config = {};
|
|
144
|
+
if (systemInstruction) config.systemInstruction = systemInstruction;
|
|
145
|
+
if (tools.length > 0) config.tools = this.formatTools(tools);
|
|
146
|
+
if (options.signal) config.abortSignal = options.signal;
|
|
147
|
+
const maxOutputTokens = Number(options.maxTokens);
|
|
148
|
+
if (Number.isFinite(maxOutputTokens) && maxOutputTokens > 0) {
|
|
149
|
+
config.maxOutputTokens = Math.floor(maxOutputTokens);
|
|
150
|
+
}
|
|
151
|
+
return config;
|
|
152
|
+
}
|
|
153
|
+
|
|
86
154
|
convertMessages(messages) {
|
|
87
155
|
let systemInstruction = '';
|
|
88
156
|
const history = [];
|
|
@@ -93,16 +161,16 @@ class GoogleProvider extends BaseProvider {
|
|
|
93
161
|
continue;
|
|
94
162
|
}
|
|
95
163
|
if (msg.role === 'tool') {
|
|
164
|
+
const functionResponse = {
|
|
165
|
+
name: msg.name || 'tool',
|
|
166
|
+
response: { result: msg.content }
|
|
167
|
+
};
|
|
168
|
+
if (msg.tool_call_id) functionResponse.id = msg.tool_call_id;
|
|
96
169
|
history.push({
|
|
97
|
-
//
|
|
98
|
-
//
|
|
170
|
+
// The current Gemini API represents tool results as functionResponse
|
|
171
|
+
// parts on a user turn.
|
|
99
172
|
role: 'user',
|
|
100
|
-
parts: [{
|
|
101
|
-
functionResponse: {
|
|
102
|
-
name: msg.name || 'tool',
|
|
103
|
-
response: { result: msg.content }
|
|
104
|
-
}
|
|
105
|
-
}]
|
|
173
|
+
parts: [{ functionResponse }]
|
|
106
174
|
});
|
|
107
175
|
continue;
|
|
108
176
|
}
|
|
@@ -113,9 +181,10 @@ class GoogleProvider extends BaseProvider {
|
|
|
113
181
|
const functionCallPart = {
|
|
114
182
|
functionCall: {
|
|
115
183
|
name: tc.function.name,
|
|
116
|
-
args:
|
|
184
|
+
args: parseToolArguments(tc.function.arguments)
|
|
117
185
|
}
|
|
118
186
|
};
|
|
187
|
+
if (tc.id) functionCallPart.functionCall.id = tc.id;
|
|
119
188
|
if (tc.function.thought_signature) {
|
|
120
189
|
functionCallPart.thoughtSignature = tc.function.thought_signature;
|
|
121
190
|
}
|
|
@@ -149,8 +218,8 @@ class GoogleProvider extends BaseProvider {
|
|
|
149
218
|
normalizedHistory.push({ role: currentRole, parts: currentParts });
|
|
150
219
|
}
|
|
151
220
|
|
|
152
|
-
// Structured helpers can consist entirely of system instructions. Gemini
|
|
153
|
-
//
|
|
221
|
+
// Structured helpers can consist entirely of system instructions. Gemini
|
|
222
|
+
// still requires a user turn to trigger generation.
|
|
154
223
|
if (
|
|
155
224
|
normalizedHistory.length === 0
|
|
156
225
|
|| normalizedHistory[normalizedHistory.length - 1].role === 'model'
|
|
@@ -174,53 +243,18 @@ class GoogleProvider extends BaseProvider {
|
|
|
174
243
|
async chat(messages, tools = [], options = {}) {
|
|
175
244
|
const model = options.model || this.config.model || this.getDefaultModel();
|
|
176
245
|
const { systemInstruction, history } = this.convertMessages(messages);
|
|
177
|
-
|
|
178
|
-
const genModel = this.genAI.getGenerativeModel({
|
|
246
|
+
const response = await this.genAI.models.generateContent({
|
|
179
247
|
model,
|
|
180
|
-
|
|
181
|
-
|
|
248
|
+
contents: history,
|
|
249
|
+
config: this.buildGenerateConfig(systemInstruction, tools, options)
|
|
182
250
|
});
|
|
183
|
-
|
|
184
|
-
const lastMessage = history.pop();
|
|
185
|
-
const chat = genModel.startChat({ history });
|
|
186
|
-
const result = await chat.sendMessage(lastMessage.parts, { signal: options.signal });
|
|
187
|
-
const response = result.response;
|
|
188
|
-
|
|
189
|
-
let content = '';
|
|
190
251
|
const toolCalls = [];
|
|
191
|
-
|
|
192
|
-
for (const candidate of response.candidates || []) {
|
|
193
|
-
for (const part of candidate.content?.parts || []) {
|
|
194
|
-
if (part.text) content += part.text;
|
|
195
|
-
if (part.functionCall) {
|
|
196
|
-
toolCalls.push({
|
|
197
|
-
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
198
|
-
type: 'function',
|
|
199
|
-
function: {
|
|
200
|
-
name: part.functionCall.name,
|
|
201
|
-
arguments: JSON.stringify(part.functionCall.args || {}),
|
|
202
|
-
thought_signature: part.thoughtSignature
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const usage = response.usageMetadata;
|
|
252
|
+
const content = collectResponseParts(response, toolCalls);
|
|
210
253
|
return {
|
|
211
254
|
content,
|
|
212
255
|
toolCalls,
|
|
213
256
|
finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
|
|
214
|
-
usage:
|
|
215
|
-
inputTokens: usage.promptTokenCount || 0,
|
|
216
|
-
outputTokens: usage.candidatesTokenCount || 0,
|
|
217
|
-
reasoningTokens: usage.thoughtsTokenCount || 0,
|
|
218
|
-
cachedReadTokens: usage.cachedContentTokenCount || 0,
|
|
219
|
-
cacheWriteTokens: 0,
|
|
220
|
-
promptTokens: usage.promptTokenCount || 0,
|
|
221
|
-
completionTokens: usage.candidatesTokenCount || 0,
|
|
222
|
-
totalTokens: usage.totalTokenCount || 0
|
|
223
|
-
} : null,
|
|
257
|
+
usage: normalizeUsage(response.usageMetadata),
|
|
224
258
|
model
|
|
225
259
|
};
|
|
226
260
|
}
|
|
@@ -228,60 +262,32 @@ class GoogleProvider extends BaseProvider {
|
|
|
228
262
|
async *stream(messages, tools = [], options = {}) {
|
|
229
263
|
const model = options.model || this.config.model || this.getDefaultModel();
|
|
230
264
|
const { systemInstruction, history } = this.convertMessages(messages);
|
|
231
|
-
|
|
232
|
-
const genModel = this.genAI.getGenerativeModel({
|
|
265
|
+
const responseStream = await this.genAI.models.generateContentStream({
|
|
233
266
|
model,
|
|
234
|
-
|
|
235
|
-
|
|
267
|
+
contents: history,
|
|
268
|
+
config: this.buildGenerateConfig(systemInstruction, tools, options)
|
|
236
269
|
});
|
|
237
|
-
|
|
238
|
-
const lastMessage = history.pop();
|
|
239
|
-
const chat = genModel.startChat({ history });
|
|
240
|
-
const result = await chat.sendMessageStream(lastMessage.parts, { signal: options.signal });
|
|
241
|
-
|
|
242
270
|
let content = '';
|
|
243
271
|
const toolCalls = [];
|
|
272
|
+
const seenToolCalls = new Set();
|
|
273
|
+
let usage = null;
|
|
244
274
|
|
|
245
|
-
for await (const chunk of
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
yield { type: 'content', content: part.text };
|
|
251
|
-
}
|
|
252
|
-
if (part.functionCall) {
|
|
253
|
-
toolCalls.push({
|
|
254
|
-
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
255
|
-
type: 'function',
|
|
256
|
-
function: {
|
|
257
|
-
name: part.functionCall.name,
|
|
258
|
-
arguments: JSON.stringify(part.functionCall.args || {}),
|
|
259
|
-
thought_signature: part.thoughtSignature
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
}
|
|
275
|
+
for await (const chunk of responseStream) {
|
|
276
|
+
const chunkContent = collectResponseParts(chunk, toolCalls, seenToolCalls);
|
|
277
|
+
if (chunkContent) {
|
|
278
|
+
content += chunkContent;
|
|
279
|
+
yield { type: 'content', content: chunkContent };
|
|
264
280
|
}
|
|
281
|
+
if (chunk.usageMetadata) usage = chunk.usageMetadata;
|
|
265
282
|
}
|
|
266
283
|
|
|
267
|
-
const finalResponse = await result.response;
|
|
268
|
-
const usage = finalResponse.usageMetadata;
|
|
269
|
-
|
|
270
284
|
yield {
|
|
271
285
|
type: 'done',
|
|
272
286
|
content,
|
|
273
287
|
toolCalls,
|
|
274
288
|
finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
|
|
275
|
-
usage: usage
|
|
276
|
-
|
|
277
|
-
outputTokens: usage.candidatesTokenCount || 0,
|
|
278
|
-
reasoningTokens: usage.thoughtsTokenCount || 0,
|
|
279
|
-
cachedReadTokens: usage.cachedContentTokenCount || 0,
|
|
280
|
-
cacheWriteTokens: 0,
|
|
281
|
-
promptTokens: usage.promptTokenCount || 0,
|
|
282
|
-
completionTokens: usage.candidatesTokenCount || 0,
|
|
283
|
-
totalTokens: usage.totalTokenCount || 0
|
|
284
|
-
} : null
|
|
289
|
+
usage: normalizeUsage(usage),
|
|
290
|
+
model
|
|
285
291
|
};
|
|
286
292
|
}
|
|
287
293
|
}
|
|
@@ -10,7 +10,13 @@ function normalizeReply(content) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function hasExternalBlocker(text) {
|
|
13
|
-
return /\b(?:blocked|cannot|can't|could not|couldn't|unable to|do not have access|don't have access|missing (?:access|credentials|permission|information)|need you to|requires? your|waiting for your|please (?:provide|send|choose|confirm|authorize|approve))\b/.test(text);
|
|
13
|
+
return /\b(?:blocked|cannot|can't|could not|couldn't|unable to|do not have access|don't have access|missing (?:access|credentials|permission|information)|need you to|requires? your|waiting for your|please (?:provide|send|choose|confirm|authorize|approve)|blockiert|kann nicht|konnte nicht|mir fehlt|ich brauche von dir|warte auf dein(?:e|en)?|bitte (?:gib|schick|sende|nenn|bestätige|waehle|wähle|genehmige))\b/.test(text);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isTerminalQuestionOrBlockerReply(content) {
|
|
17
|
+
const text = normalizeReply(content);
|
|
18
|
+
if (!text) return false;
|
|
19
|
+
return /[??]/.test(text) || hasExternalBlocker(text);
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
/**
|
|
@@ -36,10 +42,16 @@ function isDeferredWorkReply(content) {
|
|
|
36
42
|
/^(?:working on it|checking now|on it)[.!…]*$/,
|
|
37
43
|
/\bi(?:'ll| will)\s+(?:get back to you|update you|report back|let you know|keep you posted)\b/,
|
|
38
44
|
/\b(?:i(?:'ll| will)\s+follow up|stay tuned)\b/,
|
|
45
|
+
/^(?:(?:klar|okay|ok|alles klar)[,.!]?\s+)?ich\s+(?:(?:arbeite|pruefe|prüfe|schaue|untersuche|teste|debugge|starte)\s+(?:(?:gerade|aktuell|jetzt|noch)\b|(?:das|dies|die logs?|den code|deine anfrage|deinen auftrag)\b)|(?:kuemmere|kümmere)\s+mich\s+(?:gerade|aktuell|jetzt|noch)\b)/,
|
|
46
|
+
/^(?:(?:klar|okay|ok|alles klar)[,.!]?\s+)?ich\s+(?:werde|wuerde|würde)\s+(?:jetzt\s+)?(?:pruefen|prüfen|nachsehen|untersuchen|testen|debuggen|starten|fixen|erledigen)\b/,
|
|
47
|
+
/^(?:(?:klar|okay|ok|alles klar)[,.!]?\s+)?(?:lass|lasst)\s+mich\s+(?:(?:das|dies|die logs?|den code)\s+)?(?:pruefen|prüfen|nachsehen|untersuchen|testen|debuggen)\b/,
|
|
48
|
+
/^(?:gib mir|gebt mir)\s+(?:einen\s+)?(?:moment|augenblick)|^(?:bin dran|mache ich)[.!…]*$/,
|
|
49
|
+
/\bich\s+(?:melde mich|gebe dir bescheid|halte dich auf dem laufenden)\b/,
|
|
39
50
|
];
|
|
40
51
|
return patterns.some((pattern) => pattern.test(text));
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
module.exports = {
|
|
44
55
|
isDeferredWorkReply,
|
|
56
|
+
isTerminalQuestionOrBlockerReply,
|
|
45
57
|
};
|