neoagent 3.2.1-beta.1 → 3.2.1-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +654 -90
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +157 -24
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +7 -2
- package/flutter_app/lib/main_operations.dart +334 -370
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +342 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +97 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/landing/index.html +3 -1
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +115 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +2 -2
- package/server/guest-agent.cli.package.json +13 -0
- package/server/guest_agent.js +85 -40
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +12 -6
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +81930 -80509
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/behavior.js +80 -0
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +107 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +20 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/capabilityHealth.js +62 -96
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +45 -6
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +452 -176
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +291 -8
- package/server/services/ai/loop/conversation_loop.js +515 -342
- package/server/services/ai/loop/messaging_delivery.js +176 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +20 -45
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +48 -21
- package/server/services/ai/messagingFallback.js +17 -17
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_failure_cache.js +108 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +68 -163
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +166 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/claudeCode.js +21 -33
- package/server/services/ai/providers/githubCopilot.js +41 -20
- package/server/services/ai/providers/google.js +135 -97
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +111 -84
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +39 -5
- package/server/services/ai/providers/openaiCodex.js +11 -4
- package/server/services/ai/providers/openrouter.js +29 -7
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/systemPrompt.js +32 -123
- package/server/services/ai/taskAnalysis.js +104 -11
- package/server/services/ai/toolEvidence.js +256 -29
- package/server/services/ai/tools.js +248 -117
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- package/server/services/behavior/config.js +251 -0
- package/server/services/behavior/defaults.js +68 -0
- package/server/services/behavior/delivery.js +176 -0
- package/server/services/behavior/index.js +43 -0
- package/server/services/behavior/model_client.js +35 -0
- package/server/services/behavior/modules/agent_identity.js +37 -0
- package/server/services/behavior/modules/channel_style.js +28 -0
- package/server/services/behavior/modules/index.js +29 -0
- package/server/services/behavior/modules/norms.js +101 -0
- package/server/services/behavior/modules/persona.js +48 -0
- package/server/services/behavior/modules/persona_prompt.js +33 -0
- package/server/services/behavior/modules/social_memory.js +86 -0
- package/server/services/behavior/modules/social_observability.js +94 -0
- package/server/services/behavior/modules/social_signals.js +41 -0
- package/server/services/behavior/modules/theory_of_mind.js +110 -0
- package/server/services/behavior/modules/turn_taking.js +237 -0
- package/server/services/behavior/pipeline.js +285 -0
- package/server/services/behavior/registry.js +78 -0
- package/server/services/behavior/signals.js +107 -0
- package/server/services/behavior/state.js +99 -0
- package/server/services/behavior/system_prompt.js +75 -0
- package/server/services/browser/controller.js +843 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +15 -1
- package/server/services/browser/extension/provider.js +71 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/credentials/bitwarden_cli.js +322 -0
- package/server/services/credentials/broker.js +594 -0
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/bitwarden/constants.js +14 -0
- package/server/services/integrations/bitwarden/provider.js +197 -0
- package/server/services/integrations/bitwarden/snapshot.js +65 -0
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +159 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +110 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +66 -52
- package/server/services/messaging/access_policy.js +10 -6
- package/server/services/messaging/automation.js +240 -34
- package/server/services/messaging/discord.js +11 -1
- package/server/services/messaging/formatting_guides.js +5 -4
- package/server/services/messaging/http_platforms.js +37 -16
- package/server/services/messaging/inbound_queue.js +143 -28
- package/server/services/messaging/inbound_store.js +257 -0
- package/server/services/messaging/manager.js +344 -51
- package/server/services/messaging/telegram.js +10 -1
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +33 -14
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +227 -67
- package/server/services/runtime/docker-vm-manager.js +9 -0
- package/server/services/runtime/guest_bootstrap.js +30 -4
- package/server/services/runtime/guest_image.js +43 -12
- package/server/services/runtime/manager.js +77 -23
- package/server/services/runtime/validation.js +7 -6
- package/server/services/security/tool_categories.js +6 -0
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +13 -8
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/captions.js +2 -2
- package/server/services/social_video/service.js +343 -68
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/message.js +1 -1
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtime.js +2 -2
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- package/server/utils/logger.js +19 -0
- package/server/utils/retry.js +107 -0
|
@@ -6,7 +6,12 @@ const {
|
|
|
6
6
|
const {
|
|
7
7
|
normalizeOutgoingMessage,
|
|
8
8
|
} = require('../messagingFallback');
|
|
9
|
-
const { withProviderRetry
|
|
9
|
+
const { withProviderRetry } = require('../providerRetry');
|
|
10
|
+
const {
|
|
11
|
+
abortableDelay,
|
|
12
|
+
getErrorCode,
|
|
13
|
+
getHttpStatus,
|
|
14
|
+
} = require('../../../utils/retry');
|
|
10
15
|
const { shortenRunId, summarizeForLog } = require('../logFormat');
|
|
11
16
|
const {
|
|
12
17
|
TICK_MS,
|
|
@@ -16,6 +21,15 @@ const {
|
|
|
16
21
|
evaluateProgressLiveness,
|
|
17
22
|
} = require('./progress_monitor');
|
|
18
23
|
|
|
24
|
+
const SAFE_PRE_DELIVERY_NETWORK_CODES = new Set([
|
|
25
|
+
'EAI_AGAIN',
|
|
26
|
+
'ENOTFOUND',
|
|
27
|
+
'ECONNREFUSED',
|
|
28
|
+
'ENETUNREACH',
|
|
29
|
+
'EHOSTUNREACH',
|
|
30
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
31
|
+
]);
|
|
32
|
+
|
|
19
33
|
function isoNow() {
|
|
20
34
|
return new Date().toISOString();
|
|
21
35
|
}
|
|
@@ -39,9 +53,26 @@ function requireSuccessfulMessagingDelivery(result, label = 'Messaging delivery'
|
|
|
39
53
|
const error = new Error(`${label} failed: ${reason}`);
|
|
40
54
|
error.code = 'MESSAGING_DELIVERY_FAILED';
|
|
41
55
|
error.deliveryResult = result || null;
|
|
56
|
+
error.safeToRetry = result?.safeToRetry === true;
|
|
57
|
+
error.deliveryAmbiguous = result?.deliveryAmbiguous === true;
|
|
42
58
|
throw error;
|
|
43
59
|
}
|
|
44
60
|
|
|
61
|
+
function isSafeMessagingDeliveryRetry(error) {
|
|
62
|
+
if (!error || error.retryable === false || error.deliveryAmbiguous === true) return false;
|
|
63
|
+
if (error.safeToRetry === true) return true;
|
|
64
|
+
const result = error.deliveryResult;
|
|
65
|
+
if (result?.deliveryAmbiguous === true || result?.retryable === false) return false;
|
|
66
|
+
if (result?.success === false) return true;
|
|
67
|
+
|
|
68
|
+
// A rate-limit response explicitly rejected the request. DNS/connect failures
|
|
69
|
+
// happen before a platform can accept it. Timeouts, resets, broken pipes, and
|
|
70
|
+
// generic 5xx responses are intentionally excluded because the message may
|
|
71
|
+
// already have been accepted and retrying would duplicate it.
|
|
72
|
+
if (getHttpStatus(error) === 429) return true;
|
|
73
|
+
return SAFE_PRE_DELIVERY_NETWORK_CODES.has(String(getErrorCode(error) || ''));
|
|
74
|
+
}
|
|
75
|
+
|
|
45
76
|
// Harness-driven progress: the originating chat must see autonomous updates during
|
|
46
77
|
// long runs even when a weak model never calls send_interim_update itself. The
|
|
47
78
|
// supervisor paces this call (FIRST/REPEAT thresholds + a repeat guard). The update
|
|
@@ -54,6 +85,9 @@ async function sendRuntimeMessagingHeartbeat(engine, runId, options = {}) {
|
|
|
54
85
|
if (runMeta.triggerSource !== 'messaging') {
|
|
55
86
|
return { sent: false, skipped: true };
|
|
56
87
|
}
|
|
88
|
+
if (runMeta.messagingContext?.behavior?.isGroup === true) {
|
|
89
|
+
return { sent: false, skipped: true, reason: 'group_progress_suppressed' };
|
|
90
|
+
}
|
|
57
91
|
const createdAt = isoNow();
|
|
58
92
|
const heartbeatCount = Number(runMeta.progressLedger?.heartbeatCount || 0) + 1;
|
|
59
93
|
runMeta.lastSupervisorNudgeAt = createdAt;
|
|
@@ -66,13 +100,30 @@ async function sendRuntimeMessagingHeartbeat(engine, runId, options = {}) {
|
|
|
66
100
|
&& typeof runMeta.composeProgressUpdate === 'function') {
|
|
67
101
|
let statusMsg = '';
|
|
68
102
|
try {
|
|
69
|
-
const composed = await runMeta.composeProgressUpdate({
|
|
103
|
+
const composed = await runMeta.composeProgressUpdate({
|
|
104
|
+
stalled: options.stalled === true,
|
|
105
|
+
signal: runMeta.messagingProgressSupervisor?.abortController?.signal || null,
|
|
106
|
+
});
|
|
70
107
|
if (normalizeOutgoingMessage(composed, platform)) statusMsg = String(composed).trim();
|
|
71
108
|
} catch { /* no dynamic update available this tick */ }
|
|
109
|
+
const currentRunMeta = engine.getRunMeta(runId);
|
|
110
|
+
if (
|
|
111
|
+
currentRunMeta !== runMeta
|
|
112
|
+
|| runMeta.aborted
|
|
113
|
+
|| runMeta.status !== 'running'
|
|
114
|
+
|| runMeta.terminalInterim
|
|
115
|
+
|| runMeta.finalDeliverySent
|
|
116
|
+
|| runMeta.deliveryState?.finalContentDelivered === true
|
|
117
|
+
) {
|
|
118
|
+
return { sent: false, skipped: true, terminal: true };
|
|
119
|
+
}
|
|
72
120
|
if (statusMsg) try {
|
|
73
121
|
const delivery = await engine.messagingManager.sendMessage(runMeta.userId, platform, chatId, statusMsg, {
|
|
74
122
|
runId,
|
|
75
123
|
agentId: runMeta.agentId,
|
|
124
|
+
signal: runMeta.messagingProgressSupervisor?.abortController?.signal
|
|
125
|
+
|| runMeta.abortController?.signal
|
|
126
|
+
|| null,
|
|
76
127
|
});
|
|
77
128
|
if (delivery?.success === true && delivery?.suppressed !== true) {
|
|
78
129
|
const nowIso = isoNow();
|
|
@@ -129,6 +180,7 @@ function shouldSendMessagingFinalFallback(_engine, runMeta, content, platform =
|
|
|
129
180
|
return Boolean(
|
|
130
181
|
cleanedContent
|
|
131
182
|
&& !runMeta?.terminalInterim
|
|
183
|
+
&& runMeta?.noResponse !== true
|
|
132
184
|
&& runMeta?.explicitMessageSent !== true
|
|
133
185
|
&& runMeta?.finalDeliverySent !== true
|
|
134
186
|
&& runMeta?.deliveryState?.finalContentDelivered !== true
|
|
@@ -153,15 +205,59 @@ async function deliverMessagingFinalFallback(engine, {
|
|
|
153
205
|
return { sent: false, skipped: true };
|
|
154
206
|
}
|
|
155
207
|
|
|
208
|
+
const behavior = runMeta.messagingContext?.behavior;
|
|
209
|
+
const behaviorPipeline = engine.app?.locals?.behaviorPipeline;
|
|
210
|
+
if (
|
|
211
|
+
behavior
|
|
212
|
+
&& behavior.enabled !== false
|
|
213
|
+
&& behaviorPipeline
|
|
214
|
+
&& typeof behaviorPipeline.refineAndMaybeDeliver === 'function'
|
|
215
|
+
) {
|
|
216
|
+
const result = await behaviorPipeline.refineAndMaybeDeliver({
|
|
217
|
+
userId,
|
|
218
|
+
agentId,
|
|
219
|
+
msg: behavior.message,
|
|
220
|
+
config: behavior.config,
|
|
221
|
+
draft: cleanedContent,
|
|
222
|
+
messagingManager: engine.messagingManager,
|
|
223
|
+
runId,
|
|
224
|
+
signal: runMeta.abortController?.signal || null,
|
|
225
|
+
turnEpoch: behavior.turnEpoch,
|
|
226
|
+
deliver: true,
|
|
227
|
+
});
|
|
228
|
+
if (!result.delivered) {
|
|
229
|
+
if (result.suppressed !== true) {
|
|
230
|
+
const error = new Error(
|
|
231
|
+
result.delivery?.error || result.delivery?.reason || 'Behavior delivery was not confirmed.',
|
|
232
|
+
);
|
|
233
|
+
error.code = 'MESSAGING_DELIVERY_FAILED';
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
runMeta.noResponse = true;
|
|
237
|
+
if (runMeta.deliveryState) runMeta.deliveryState.noResponse = true;
|
|
238
|
+
return {
|
|
239
|
+
sent: false,
|
|
240
|
+
suppressed: result.suppressed === true,
|
|
241
|
+
reason: result.reasonCodes?.[0] || 'behavior_suppressed',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
runMeta.lastSentMessage = result.content;
|
|
245
|
+
if (!Array.isArray(runMeta.sentMessages)) runMeta.sentMessages = [];
|
|
246
|
+
runMeta.sentMessages.push(result.content);
|
|
247
|
+
engine.markRunFinalDelivery(runId, result.content);
|
|
248
|
+
return { sent: true, behavior: true, content: result.content };
|
|
249
|
+
}
|
|
250
|
+
|
|
156
251
|
const chunks = splitOutgoingMessageForPlatform(platform, cleanedContent);
|
|
157
252
|
console.info(
|
|
158
253
|
`[Run ${shortenRunId(runId)}] messaging_fallback chunks=${chunks.length} to=${summarizeForLog(chatId, 80)}`
|
|
159
254
|
);
|
|
255
|
+
const deliveredChunks = [];
|
|
160
256
|
for (let i = 0; i < chunks.length; i++) {
|
|
161
257
|
if (i > 0) {
|
|
162
258
|
const delay = Math.max(1000, Math.min(chunks[i].length * 30, 4000));
|
|
163
259
|
await engine.messagingManager.sendTyping(userId, platform, chatId, true, { agentId }).catch(() => {});
|
|
164
|
-
await
|
|
260
|
+
await abortableDelay(delay, runMeta.abortController?.signal || null);
|
|
165
261
|
}
|
|
166
262
|
try {
|
|
167
263
|
await withProviderRetry(async () => {
|
|
@@ -170,32 +266,34 @@ async function deliverMessagingFinalFallback(engine, {
|
|
|
170
266
|
platform,
|
|
171
267
|
chatId,
|
|
172
268
|
chunks[i],
|
|
173
|
-
{
|
|
269
|
+
{
|
|
270
|
+
runId,
|
|
271
|
+
agentId,
|
|
272
|
+
idempotencyKey: `${runId}:final:${i}`,
|
|
273
|
+
signal: runMeta.abortController?.signal || null,
|
|
274
|
+
},
|
|
174
275
|
);
|
|
175
276
|
return requireSuccessfulMessagingDelivery(deliveryResult, 'Final messaging delivery');
|
|
176
277
|
}, {
|
|
177
278
|
...engine.messagingDeliveryRetry,
|
|
178
279
|
label: `MessagingDelivery ${platform}`,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
&& (
|
|
182
|
-
error?.code === 'MESSAGING_DELIVERY_FAILED'
|
|
183
|
-
|| isTransientError(error)
|
|
184
|
-
)
|
|
185
|
-
),
|
|
280
|
+
signal: runMeta.abortController?.signal || null,
|
|
281
|
+
isRetryable: isSafeMessagingDeliveryRetry,
|
|
186
282
|
});
|
|
187
283
|
} catch (error) {
|
|
188
284
|
error.disableAutonomousRetry = true;
|
|
285
|
+
error.deliveredChunks = deliveredChunks.slice();
|
|
189
286
|
throw error;
|
|
190
287
|
}
|
|
288
|
+
deliveredChunks.push(chunks[i]);
|
|
289
|
+
runMeta.lastSentMessage = chunks[i];
|
|
290
|
+
if (!Array.isArray(runMeta.sentMessages)) runMeta.sentMessages = [];
|
|
291
|
+
runMeta.sentMessages.push(chunks[i]);
|
|
191
292
|
}
|
|
192
293
|
|
|
193
|
-
runMeta.lastSentMessage =
|
|
194
|
-
runMeta.sentMessages = Array.isArray(runMeta.sentMessages)
|
|
195
|
-
? [...runMeta.sentMessages, ...chunks]
|
|
196
|
-
: chunks.slice();
|
|
294
|
+
runMeta.lastSentMessage = deliveredChunks[deliveredChunks.length - 1] || cleanedContent;
|
|
197
295
|
engine.markRunFinalDelivery(runId, runMeta.lastSentMessage);
|
|
198
|
-
return { sent: true, chunks };
|
|
296
|
+
return { sent: true, chunks: deliveredChunks };
|
|
199
297
|
}
|
|
200
298
|
|
|
201
299
|
async function tickMessagingProgressSupervisor(engine, runId) {
|
|
@@ -206,54 +304,71 @@ async function tickMessagingProgressSupervisor(engine, runId) {
|
|
|
206
304
|
if (runMeta.terminalInterim) {
|
|
207
305
|
return { sent: false, skipped: true };
|
|
208
306
|
}
|
|
307
|
+
if (runMeta.progressSupervisorTickInFlight === true) {
|
|
308
|
+
return { sent: false, skipped: true, inFlight: true };
|
|
309
|
+
}
|
|
310
|
+
runMeta.progressSupervisorTickInFlight = true;
|
|
311
|
+
let settleTick;
|
|
312
|
+
const tickPromise = new Promise((resolve) => { settleTick = resolve; });
|
|
313
|
+
runMeta.progressSupervisorTickPromise = tickPromise;
|
|
209
314
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
runMeta.progressLedger = ledger;
|
|
214
|
-
const liveness = evaluateProgressLiveness(runMeta);
|
|
215
|
-
const stalled = liveness.stalled;
|
|
216
|
-
|
|
217
|
-
if (stalled && !ledger.stallNotifiedAt) {
|
|
218
|
-
engine.updateRunProgress(runId, {
|
|
219
|
-
stallNotifiedAt: isoNow(),
|
|
220
|
-
progressState: 'stalled',
|
|
315
|
+
try {
|
|
316
|
+
const ledger = runMeta.progressLedger || buildInitialProgressLedger({
|
|
317
|
+
startedAt: runMeta.startedAtIso || isoNow(),
|
|
221
318
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
phase: ledger.currentPhase || 'idle',
|
|
226
|
-
}, { agentId: runMeta.agentId });
|
|
227
|
-
}
|
|
319
|
+
runMeta.progressLedger = ledger;
|
|
320
|
+
const liveness = evaluateProgressLiveness(runMeta);
|
|
321
|
+
const stalled = liveness.stalled;
|
|
228
322
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
323
|
+
if (stalled && !ledger.stallNotifiedAt) {
|
|
324
|
+
engine.updateRunProgress(runId, {
|
|
325
|
+
stallNotifiedAt: isoNow(),
|
|
326
|
+
progressState: 'stalled',
|
|
327
|
+
});
|
|
328
|
+
engine.recordRunEvent(runMeta.userId, runId, 'progress_stalled', {
|
|
329
|
+
currentTool: ledger.currentTool || null,
|
|
330
|
+
currentStep: ledger.currentStep || null,
|
|
331
|
+
phase: ledger.currentPhase || 'idle',
|
|
332
|
+
}, { agentId: runMeta.agentId });
|
|
333
|
+
}
|
|
232
334
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
335
|
+
if (!liveness.shouldNudge) {
|
|
336
|
+
return { sent: false, skipped: true };
|
|
337
|
+
}
|
|
237
338
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
339
|
+
const lastSupervisorNudgeAtMs = timestampMs(runMeta.lastSupervisorNudgeAt, 0);
|
|
340
|
+
if (lastSupervisorNudgeAtMs > 0 && (Date.now() - lastSupervisorNudgeAtMs) < REPEAT_UPDATE_MS) {
|
|
341
|
+
return { sent: false, skipped: true };
|
|
342
|
+
}
|
|
241
343
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
344
|
+
if (ledger.currentPhase === 'tool' || ledger.currentPhase === 'model') {
|
|
345
|
+
return await sendRuntimeMessagingHeartbeat(engine, runId, { stalled });
|
|
346
|
+
}
|
|
245
347
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
348
|
+
if (ledger.currentPhase !== 'idle') {
|
|
349
|
+
return { sent: false, skipped: true };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const queued = engine.enqueueSystemSteering(runId, buildProgressNudge({ stalled }), {
|
|
353
|
+
reason: stalled ? 'stalled_progress_check' : 'progress_check',
|
|
354
|
+
});
|
|
355
|
+
if (!queued) {
|
|
356
|
+
return { sent: false, skipped: true };
|
|
357
|
+
}
|
|
358
|
+
runMeta.lastSupervisorNudgeAt = isoNow();
|
|
359
|
+
engine.updateRunProgress(runId, {
|
|
360
|
+
lastUserVisibleUpdateAt: ledger.lastUserVisibleUpdateAt || null,
|
|
361
|
+
});
|
|
362
|
+
return { sent: false, queued: true };
|
|
363
|
+
} finally {
|
|
364
|
+
settleTick();
|
|
365
|
+
if (engine.getRunMeta(runId) === runMeta) {
|
|
366
|
+
runMeta.progressSupervisorTickInFlight = false;
|
|
367
|
+
if (runMeta.progressSupervisorTickPromise === tickPromise) {
|
|
368
|
+
runMeta.progressSupervisorTickPromise = null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
251
371
|
}
|
|
252
|
-
runMeta.lastSupervisorNudgeAt = isoNow();
|
|
253
|
-
engine.updateRunProgress(runId, {
|
|
254
|
-
lastUserVisibleUpdateAt: ledger.lastUserVisibleUpdateAt || null,
|
|
255
|
-
});
|
|
256
|
-
return { sent: false, queued: true };
|
|
257
372
|
}
|
|
258
373
|
|
|
259
374
|
function startMessagingProgressSupervisor(engine, runId) {
|
|
@@ -264,25 +379,29 @@ function startMessagingProgressSupervisor(engine, runId) {
|
|
|
264
379
|
if (runMeta.messagingProgressSupervisor?.timer) {
|
|
265
380
|
return true;
|
|
266
381
|
}
|
|
382
|
+
const abortController = new AbortController();
|
|
267
383
|
const timer = setInterval(() => {
|
|
268
384
|
tickMessagingProgressSupervisor(engine, runId).catch((error) => {
|
|
269
385
|
console.warn('[Engine] Messaging progress supervisor failed:', error?.message || error);
|
|
270
386
|
});
|
|
271
387
|
}, TICK_MS);
|
|
272
388
|
timer.unref?.();
|
|
273
|
-
runMeta.messagingProgressSupervisor = { timer };
|
|
389
|
+
runMeta.messagingProgressSupervisor = { timer, abortController };
|
|
274
390
|
return true;
|
|
275
391
|
}
|
|
276
392
|
|
|
277
393
|
function stopMessagingProgressSupervisor(engine, runId) {
|
|
278
394
|
const runMeta = engine.getRunMeta(runId);
|
|
395
|
+
const inFlight = runMeta?.progressSupervisorTickPromise || null;
|
|
279
396
|
const timer = runMeta?.messagingProgressSupervisor?.timer || null;
|
|
280
397
|
if (timer) {
|
|
281
398
|
clearInterval(timer);
|
|
282
399
|
}
|
|
400
|
+
runMeta?.messagingProgressSupervisor?.abortController?.abort('Progress supervisor stopped.');
|
|
283
401
|
if (runMeta?.messagingProgressSupervisor) {
|
|
284
402
|
runMeta.messagingProgressSupervisor = null;
|
|
285
403
|
}
|
|
404
|
+
return inFlight || Promise.resolve();
|
|
286
405
|
}
|
|
287
406
|
|
|
288
407
|
module.exports = {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MODEL_CALL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
4
|
+
|
|
5
|
+
function formatElapsedDuration(durationMs) {
|
|
6
|
+
const totalSeconds = Math.max(1, Math.floor(Number(durationMs || 0) / 1000));
|
|
7
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
8
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
9
|
+
const seconds = totalSeconds % 60;
|
|
10
|
+
if (seconds === 0) return `${minutes}m`;
|
|
11
|
+
return `${minutes}m ${seconds}s`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveModelCallTimeoutMs(options = {}) {
|
|
15
|
+
const requested = Number(options?.modelCallTimeoutMs);
|
|
16
|
+
if (Number.isFinite(requested) && requested > 0) {
|
|
17
|
+
return Math.max(10, requested);
|
|
18
|
+
}
|
|
19
|
+
return MODEL_CALL_TIMEOUT_MS;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function abortError(reason = null) {
|
|
23
|
+
if (reason instanceof Error) return reason;
|
|
24
|
+
const error = new Error(String(reason || 'Model call aborted.'));
|
|
25
|
+
error.name = 'AbortError';
|
|
26
|
+
error.code = 'ABORT_ERR';
|
|
27
|
+
return error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function withModelCallTimeout(promise, options = {}, label = 'Model call') {
|
|
31
|
+
const timeoutMs = resolveModelCallTimeoutMs(options);
|
|
32
|
+
const signal = options?.modelAbortController?.signal || options?.signal || null;
|
|
33
|
+
let timer = null;
|
|
34
|
+
let abortListener = null;
|
|
35
|
+
const timeout = new Promise((_, reject) => {
|
|
36
|
+
timer = setTimeout(() => {
|
|
37
|
+
const error = new Error(`${label} timed out after ${formatElapsedDuration(timeoutMs)}.`);
|
|
38
|
+
error.code = 'MODEL_CALL_TIMEOUT';
|
|
39
|
+
options?.modelAbortController?.abort(error);
|
|
40
|
+
reject(error);
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
});
|
|
43
|
+
const aborted = new Promise((_, reject) => {
|
|
44
|
+
if (!signal) return;
|
|
45
|
+
abortListener = () => reject(abortError(signal.reason));
|
|
46
|
+
if (signal.aborted) abortListener();
|
|
47
|
+
else signal.addEventListener('abort', abortListener, { once: true });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
return await Promise.race([Promise.resolve(promise), timeout, aborted]);
|
|
52
|
+
} finally {
|
|
53
|
+
if (timer) clearTimeout(timer);
|
|
54
|
+
if (abortListener) signal?.removeEventListener('abort', abortListener);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runAbortableModelCall(factory, options = {}, label = 'Model call') {
|
|
59
|
+
const modelAbortController = new AbortController();
|
|
60
|
+
const parentSignals = [...new Set([
|
|
61
|
+
options?.signal,
|
|
62
|
+
...(Array.isArray(options?.signals) ? options.signals : []),
|
|
63
|
+
].filter((signal) => signal && typeof signal.addEventListener === 'function'))];
|
|
64
|
+
const parentListeners = new Map();
|
|
65
|
+
for (const parentSignal of parentSignals) {
|
|
66
|
+
const abortFromParent = () => modelAbortController.abort(parentSignal.reason);
|
|
67
|
+
parentListeners.set(parentSignal, abortFromParent);
|
|
68
|
+
if (parentSignal.aborted) abortFromParent();
|
|
69
|
+
else parentSignal.addEventListener('abort', abortFromParent, { once: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const promise = Promise.resolve().then(() => factory(modelAbortController.signal));
|
|
74
|
+
return await withModelCallTimeout(
|
|
75
|
+
promise,
|
|
76
|
+
{ ...options, modelAbortController },
|
|
77
|
+
label,
|
|
78
|
+
);
|
|
79
|
+
} finally {
|
|
80
|
+
for (const [parentSignal, abortFromParent] of parentListeners) {
|
|
81
|
+
parentSignal.removeEventListener('abort', abortFromParent);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
abortError,
|
|
88
|
+
resolveModelCallTimeoutMs,
|
|
89
|
+
runAbortableModelCall,
|
|
90
|
+
withModelCallTimeout,
|
|
91
|
+
};
|
|
@@ -5,48 +5,16 @@ const { sanitizeModelOutput } = require('../outputSanitizer');
|
|
|
5
5
|
const { parseJsonObject } = require('../taskAnalysis');
|
|
6
6
|
const { withProviderRetry, isTransientError } = require('../providerRetry');
|
|
7
7
|
const { normalizeUsage, recordModelUsage } = require('../usage');
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const {
|
|
9
|
+
resolveModelCallTimeoutMs,
|
|
10
|
+
runAbortableModelCall,
|
|
11
|
+
withModelCallTimeout,
|
|
12
|
+
} = require('./model_call_guard');
|
|
10
13
|
|
|
11
14
|
function isoNow() {
|
|
12
15
|
return new Date().toISOString();
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
function formatElapsedDuration(durationMs) {
|
|
16
|
-
const totalSeconds = Math.max(1, Math.floor(Number(durationMs || 0) / 1000));
|
|
17
|
-
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
18
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
19
|
-
const seconds = totalSeconds % 60;
|
|
20
|
-
if (seconds === 0) return `${minutes}m`;
|
|
21
|
-
return `${minutes}m ${seconds}s`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function resolveModelCallTimeoutMs(options = {}) {
|
|
25
|
-
const requested = Number(options?.modelCallTimeoutMs);
|
|
26
|
-
if (Number.isFinite(requested) && requested > 0) {
|
|
27
|
-
return Math.max(10, requested);
|
|
28
|
-
}
|
|
29
|
-
return MODEL_CALL_TIMEOUT_MS;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function withModelCallTimeout(promise, options = {}, label = 'Model call') {
|
|
33
|
-
const timeoutMs = resolveModelCallTimeoutMs(options);
|
|
34
|
-
let timer = null;
|
|
35
|
-
const timeout = new Promise((_, reject) => {
|
|
36
|
-
timer = setTimeout(() => {
|
|
37
|
-
options?.modelAbortController?.abort(`${label} timed out.`);
|
|
38
|
-
const error = new Error(`${label} timed out after ${formatElapsedDuration(timeoutMs)}.`);
|
|
39
|
-
error.code = 'MODEL_CALL_TIMEOUT';
|
|
40
|
-
reject(error);
|
|
41
|
-
}, timeoutMs);
|
|
42
|
-
});
|
|
43
|
-
try {
|
|
44
|
-
return await Promise.race([Promise.resolve(promise), timeout]);
|
|
45
|
-
} finally {
|
|
46
|
-
if (timer) clearTimeout(timer);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
18
|
async function requestStructuredJson(engine, {
|
|
51
19
|
provider,
|
|
52
20
|
providerName,
|
|
@@ -62,7 +30,11 @@ async function requestStructuredJson(engine, {
|
|
|
62
30
|
}) {
|
|
63
31
|
const startedAt = Date.now();
|
|
64
32
|
const structuredStep = `model:${phase}`;
|
|
65
|
-
const
|
|
33
|
+
const modelAbortController = new AbortController();
|
|
34
|
+
const parentSignal = telemetry?.signal;
|
|
35
|
+
const abortFromParent = () => modelAbortController.abort(parentSignal?.reason);
|
|
36
|
+
if (parentSignal?.aborted) abortFromParent();
|
|
37
|
+
else parentSignal?.addEventListener('abort', abortFromParent, { once: true });
|
|
66
38
|
if (telemetry?.runId) {
|
|
67
39
|
engine.updateRunProgress(telemetry.runId, {
|
|
68
40
|
currentPhase: 'model',
|
|
@@ -72,7 +44,6 @@ async function requestStructuredJson(engine, {
|
|
|
72
44
|
});
|
|
73
45
|
}
|
|
74
46
|
|
|
75
|
-
let completed = false;
|
|
76
47
|
try {
|
|
77
48
|
const response = await withProviderRetry(
|
|
78
49
|
() => withModelCallTimeout(
|
|
@@ -86,15 +57,18 @@ async function requestStructuredJson(engine, {
|
|
|
86
57
|
model,
|
|
87
58
|
maxTokens,
|
|
88
59
|
reasoningEffort: reasoningEffort || engine.getReasoningEffort(providerName, {}),
|
|
89
|
-
signal,
|
|
60
|
+
signal: modelAbortController.signal,
|
|
90
61
|
}
|
|
91
62
|
),
|
|
92
|
-
telemetry || {},
|
|
63
|
+
{ ...(telemetry || {}), modelAbortController },
|
|
93
64
|
`${phase} model call`,
|
|
94
65
|
),
|
|
95
|
-
{
|
|
66
|
+
{
|
|
67
|
+
label: `Engine ${model} (structured)`,
|
|
68
|
+
isRetryable: (err) => !modelAbortController.signal.aborted && isTransientError(err),
|
|
69
|
+
signal: modelAbortController.signal,
|
|
70
|
+
}
|
|
96
71
|
);
|
|
97
|
-
completed = true;
|
|
98
72
|
if (telemetry?.runId && telemetry?.userId) {
|
|
99
73
|
recordModelUsage({
|
|
100
74
|
runId: telemetry.runId,
|
|
@@ -117,6 +91,7 @@ async function requestStructuredJson(engine, {
|
|
|
117
91
|
usage: normalizedUsage?.totalTokens || 0,
|
|
118
92
|
};
|
|
119
93
|
} finally {
|
|
94
|
+
parentSignal?.removeEventListener('abort', abortFromParent);
|
|
120
95
|
const runMeta = telemetry?.runId ? engine.getRunMeta(telemetry.runId) : null;
|
|
121
96
|
if (runMeta?.progressLedger?.currentStep === structuredStep) {
|
|
122
97
|
engine.updateRunProgress(telemetry.runId, {
|
|
@@ -124,8 +99,6 @@ async function requestStructuredJson(engine, {
|
|
|
124
99
|
currentStep: null,
|
|
125
100
|
currentTool: null,
|
|
126
101
|
currentStepStartedAt: null,
|
|
127
|
-
}, {
|
|
128
|
-
verified: completed,
|
|
129
102
|
});
|
|
130
103
|
}
|
|
131
104
|
}
|
|
@@ -227,6 +200,7 @@ async function requestModelResponse(engine, {
|
|
|
227
200
|
phase: 'recovering',
|
|
228
201
|
});
|
|
229
202
|
},
|
|
203
|
+
signal: modelAbortController.signal,
|
|
230
204
|
}));
|
|
231
205
|
} finally {
|
|
232
206
|
parentSignal?.removeEventListener('abort', abortFromParent);
|
|
@@ -271,5 +245,6 @@ module.exports = {
|
|
|
271
245
|
requestModelResponse,
|
|
272
246
|
requestStructuredJson,
|
|
273
247
|
resolveModelCallTimeoutMs,
|
|
248
|
+
runAbortableModelCall,
|
|
274
249
|
withModelCallTimeout,
|
|
275
250
|
};
|
|
@@ -182,7 +182,9 @@ function buildReadOnlyChurnGuidance({ readOnlyCount = 0, alreadyRead = '' } = {}
|
|
|
182
182
|
: 'Do not re-read or re-search anything already in this conversation.',
|
|
183
183
|
'Decide from the evidence you have now.',
|
|
184
184
|
'If the requested work is already done, no matching target exists, or the available tools cannot make the change, call task_complete with that truthful final answer or blocker.',
|
|
185
|
+
'If research targets remain uncovered, open a new primary source for one uncovered target now instead of re-querying the same lead.',
|
|
185
186
|
'If exactly one concrete safe action remains, take that action now. Otherwise finish; more poking around is not progress.',
|
|
187
|
+
'Never invent missing targets, products, people, files, or outcomes to make the answer look complete.',
|
|
186
188
|
].join(' ');
|
|
187
189
|
}
|
|
188
190
|
|
|
@@ -4,7 +4,10 @@ const { v4: uuidv4 } = require('uuid');
|
|
|
4
4
|
const db = require('../../../db/database');
|
|
5
5
|
const { globalHooks } = require('../hooks');
|
|
6
6
|
const { summarizeForLog } = require('../logFormat');
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
inferToolFailureMessage,
|
|
9
|
+
resolveDeclaredToolAccess,
|
|
10
|
+
} = require('../toolEvidence');
|
|
8
11
|
|
|
9
12
|
function getAvailableTools(_engine, app, options = {}) {
|
|
10
13
|
const { getAvailableTools: loadAvailableTools } = require('../tools');
|
|
@@ -16,8 +19,21 @@ async function executeTool(engine, toolName, args, context) {
|
|
|
16
19
|
return runTool(toolName, args, context, engine);
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
function isReadOnlyToolCall(toolCall) {
|
|
22
|
+
function isReadOnlyToolCall(toolCall, toolDefinition = null) {
|
|
20
23
|
const name = String(toolCall?.function?.name || '');
|
|
24
|
+
let toolArgs = {};
|
|
25
|
+
try {
|
|
26
|
+
toolArgs = typeof toolCall?.function?.arguments === 'string'
|
|
27
|
+
? JSON.parse(toolCall.function.arguments || '{}')
|
|
28
|
+
: (toolCall?.function?.arguments || {});
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const declaredAccess = resolveDeclaredToolAccess(toolDefinition, toolArgs);
|
|
34
|
+
if (declaredAccess === 'read') return true;
|
|
35
|
+
if (declaredAccess === 'write') return false;
|
|
36
|
+
|
|
21
37
|
const readOnly = new Set([
|
|
22
38
|
'read_file',
|
|
23
39
|
'read_files',
|
|
@@ -35,12 +51,7 @@ function isReadOnlyToolCall(toolCall) {
|
|
|
35
51
|
'read_health_data',
|
|
36
52
|
]);
|
|
37
53
|
if (name === 'http_request') {
|
|
38
|
-
|
|
39
|
-
const args = JSON.parse(toolCall.function.arguments || '{}');
|
|
40
|
-
return String(args.method || 'GET').toUpperCase() === 'GET';
|
|
41
|
-
} catch {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
54
|
+
return String(toolArgs.method || 'GET').toUpperCase() === 'GET';
|
|
44
55
|
}
|
|
45
56
|
return readOnly.has(name);
|
|
46
57
|
}
|