neoagent 3.2.1-beta.0 → 3.2.1-beta.10
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/docs/agent-run-lifecycle.md +10 -0
- 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 +111 -20
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +78 -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 +145 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +4 -4
- 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 +11 -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 +73083 -72209
- package/server/routes/admin.js +1 -1
- package/server/routes/agents.js +35 -2
- package/server/routes/android.js +30 -34
- 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 +16 -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 +496 -166
- 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 +121 -5
- package/server/services/ai/loop/conversation_loop.js +620 -340
- package/server/services/ai/loop/lifecycle.js +108 -0
- package/server/services/ai/loop/messaging_delivery.js +129 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +48 -56
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +28 -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 +4 -4
- 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 +6 -5
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +12 -7
- package/server/services/ai/providers/ollama.js +114 -86
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +41 -7
- package/server/services/ai/providers/openaiCodex.js +13 -4
- package/server/services/ai/providers/openrouter.js +31 -9
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/systemPrompt.js +19 -12
- package/server/services/ai/taskAnalysis.js +58 -10
- package/server/services/ai/terminal_reply.js +18 -0
- package/server/services/ai/toolEvidence.js +350 -29
- package/server/services/ai/tools.js +190 -111
- 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/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 +52 -19
- package/server/services/messaging/automation.js +85 -10
- package/server/services/messaging/formatting_guides.js +7 -4
- package/server/services/messaging/http_platforms.js +33 -13
- package/server/services/messaging/inbound_queue.js +78 -24
- package/server/services/messaging/inbound_store.js +224 -0
- package/server/services/messaging/manager.js +326 -51
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +22 -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 +12 -7
- 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/service.js +160 -50
- 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/retry.js +107 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require('../../../db/database');
|
|
4
|
+
|
|
5
|
+
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'stopped', 'interrupted']);
|
|
6
|
+
const CONTROL_PRIORITY = Object.freeze({ pause: 1, stop: 2, interrupt: 3 });
|
|
7
|
+
|
|
8
|
+
function getRunControl(runId) {
|
|
9
|
+
return db.prepare(
|
|
10
|
+
`SELECT action, reason, requested_at
|
|
11
|
+
FROM agent_run_controls
|
|
12
|
+
WHERE run_id = ? AND consumed_at IS NULL`,
|
|
13
|
+
).get(runId) || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function requestRunControl(runId, userId, action, reason = '') {
|
|
17
|
+
if (!Object.hasOwn(CONTROL_PRIORITY, action)) {
|
|
18
|
+
throw new Error(`Unsupported run control action: ${action}`);
|
|
19
|
+
}
|
|
20
|
+
const run = db.prepare(
|
|
21
|
+
'SELECT status FROM agent_runs WHERE id = ? AND user_id = ?',
|
|
22
|
+
).get(runId, userId);
|
|
23
|
+
if (!run) return { accepted: false, reason: 'not_found' };
|
|
24
|
+
if (TERMINAL_STATUSES.has(run.status)) return { accepted: false, reason: 'terminal', status: run.status };
|
|
25
|
+
|
|
26
|
+
const existing = getRunControl(runId);
|
|
27
|
+
if (existing && CONTROL_PRIORITY[existing.action] > CONTROL_PRIORITY[action]) {
|
|
28
|
+
return { accepted: false, reason: 'stronger_signal_pending', action: existing.action };
|
|
29
|
+
}
|
|
30
|
+
db.prepare(
|
|
31
|
+
`INSERT INTO agent_run_controls (run_id, user_id, action, reason)
|
|
32
|
+
VALUES (?, ?, ?, ?)
|
|
33
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
34
|
+
action = excluded.action,
|
|
35
|
+
reason = excluded.reason,
|
|
36
|
+
requested_at = datetime('now'),
|
|
37
|
+
consumed_at = NULL`,
|
|
38
|
+
).run(runId, userId, action, String(reason || '').slice(0, 1000));
|
|
39
|
+
return { accepted: true, action };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function checkpointRun(runId, phase, state = {}) {
|
|
43
|
+
db.prepare(
|
|
44
|
+
`INSERT INTO agent_run_checkpoints (run_id, version, phase, state_json)
|
|
45
|
+
VALUES (?, 1, ?, ?)
|
|
46
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
47
|
+
version = 1,
|
|
48
|
+
phase = excluded.phase,
|
|
49
|
+
state_json = excluded.state_json,
|
|
50
|
+
updated_at = datetime('now')`,
|
|
51
|
+
).run(runId, phase, JSON.stringify(state));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function transitionRun(runId, status, fields = {}, allowed = ['running']) {
|
|
55
|
+
const assignments = ['status = ?', "updated_at = datetime('now')"];
|
|
56
|
+
const values = [status];
|
|
57
|
+
if (Object.hasOwn(fields, 'error')) {
|
|
58
|
+
assignments.push('error = ?');
|
|
59
|
+
values.push(fields.error || null);
|
|
60
|
+
}
|
|
61
|
+
if (Object.hasOwn(fields, 'finalResponse')) {
|
|
62
|
+
assignments.push('final_response = ?');
|
|
63
|
+
values.push(fields.finalResponse || null);
|
|
64
|
+
}
|
|
65
|
+
if (Object.hasOwn(fields, 'totalTokens')) {
|
|
66
|
+
assignments.push('total_tokens = ?');
|
|
67
|
+
values.push(Number(fields.totalTokens) || 0);
|
|
68
|
+
}
|
|
69
|
+
if (TERMINAL_STATUSES.has(status)) assignments.push("completed_at = COALESCE(completed_at, datetime('now'))");
|
|
70
|
+
const placeholders = allowed.map(() => '?').join(', ');
|
|
71
|
+
values.push(runId, ...allowed);
|
|
72
|
+
const result = db.prepare(
|
|
73
|
+
`UPDATE agent_runs SET ${assignments.join(', ')}
|
|
74
|
+
WHERE id = ? AND status IN (${placeholders})`,
|
|
75
|
+
).run(...values);
|
|
76
|
+
return result.changes > 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function closeRun(runId, status, fields = {}, allowed = ['running']) {
|
|
80
|
+
const transaction = db.transaction(() => {
|
|
81
|
+
if (!transitionRun(runId, status, fields, allowed)) return false;
|
|
82
|
+
db.prepare(
|
|
83
|
+
`UPDATE agent_steps
|
|
84
|
+
SET status = ?, error = COALESCE(NULLIF(error, ''), ?), completed_at = COALESCE(completed_at, datetime('now'))
|
|
85
|
+
WHERE run_id = ? AND status = 'running'`,
|
|
86
|
+
).run(status, fields.error || null, runId);
|
|
87
|
+
db.prepare(
|
|
88
|
+
`UPDATE agent_delegations
|
|
89
|
+
SET status = ?, error = COALESCE(NULLIF(error, ''), ?), updated_at = datetime('now'), completed_at = COALESCE(completed_at, datetime('now'))
|
|
90
|
+
WHERE parent_run_id = ? AND status = 'running'`,
|
|
91
|
+
).run(status, fields.error || null, runId);
|
|
92
|
+
db.prepare(
|
|
93
|
+
`UPDATE agent_run_controls SET consumed_at = datetime('now')
|
|
94
|
+
WHERE run_id = ? AND consumed_at IS NULL`,
|
|
95
|
+
).run(runId);
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
98
|
+
return transaction();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
TERMINAL_STATUSES,
|
|
103
|
+
checkpointRun,
|
|
104
|
+
closeRun,
|
|
105
|
+
getRunControl,
|
|
106
|
+
requestRunControl,
|
|
107
|
+
transitionRun,
|
|
108
|
+
};
|
|
@@ -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
|
|
@@ -66,13 +97,30 @@ async function sendRuntimeMessagingHeartbeat(engine, runId, options = {}) {
|
|
|
66
97
|
&& typeof runMeta.composeProgressUpdate === 'function') {
|
|
67
98
|
let statusMsg = '';
|
|
68
99
|
try {
|
|
69
|
-
const composed = await runMeta.composeProgressUpdate({
|
|
100
|
+
const composed = await runMeta.composeProgressUpdate({
|
|
101
|
+
stalled: options.stalled === true,
|
|
102
|
+
signal: runMeta.messagingProgressSupervisor?.abortController?.signal || null,
|
|
103
|
+
});
|
|
70
104
|
if (normalizeOutgoingMessage(composed, platform)) statusMsg = String(composed).trim();
|
|
71
105
|
} catch { /* no dynamic update available this tick */ }
|
|
106
|
+
const currentRunMeta = engine.getRunMeta(runId);
|
|
107
|
+
if (
|
|
108
|
+
currentRunMeta !== runMeta
|
|
109
|
+
|| runMeta.aborted
|
|
110
|
+
|| runMeta.status !== 'running'
|
|
111
|
+
|| runMeta.terminalInterim
|
|
112
|
+
|| runMeta.finalDeliverySent
|
|
113
|
+
|| runMeta.deliveryState?.finalContentDelivered === true
|
|
114
|
+
) {
|
|
115
|
+
return { sent: false, skipped: true, terminal: true };
|
|
116
|
+
}
|
|
72
117
|
if (statusMsg) try {
|
|
73
118
|
const delivery = await engine.messagingManager.sendMessage(runMeta.userId, platform, chatId, statusMsg, {
|
|
74
119
|
runId,
|
|
75
120
|
agentId: runMeta.agentId,
|
|
121
|
+
signal: runMeta.messagingProgressSupervisor?.abortController?.signal
|
|
122
|
+
|| runMeta.abortController?.signal
|
|
123
|
+
|| null,
|
|
76
124
|
});
|
|
77
125
|
if (delivery?.success === true && delivery?.suppressed !== true) {
|
|
78
126
|
const nowIso = isoNow();
|
|
@@ -157,11 +205,12 @@ async function deliverMessagingFinalFallback(engine, {
|
|
|
157
205
|
console.info(
|
|
158
206
|
`[Run ${shortenRunId(runId)}] messaging_fallback chunks=${chunks.length} to=${summarizeForLog(chatId, 80)}`
|
|
159
207
|
);
|
|
208
|
+
const deliveredChunks = [];
|
|
160
209
|
for (let i = 0; i < chunks.length; i++) {
|
|
161
210
|
if (i > 0) {
|
|
162
211
|
const delay = Math.max(1000, Math.min(chunks[i].length * 30, 4000));
|
|
163
212
|
await engine.messagingManager.sendTyping(userId, platform, chatId, true, { agentId }).catch(() => {});
|
|
164
|
-
await
|
|
213
|
+
await abortableDelay(delay, runMeta.abortController?.signal || null);
|
|
165
214
|
}
|
|
166
215
|
try {
|
|
167
216
|
await withProviderRetry(async () => {
|
|
@@ -170,32 +219,34 @@ async function deliverMessagingFinalFallback(engine, {
|
|
|
170
219
|
platform,
|
|
171
220
|
chatId,
|
|
172
221
|
chunks[i],
|
|
173
|
-
{
|
|
222
|
+
{
|
|
223
|
+
runId,
|
|
224
|
+
agentId,
|
|
225
|
+
idempotencyKey: `${runId}:final:${i}`,
|
|
226
|
+
signal: runMeta.abortController?.signal || null,
|
|
227
|
+
},
|
|
174
228
|
);
|
|
175
229
|
return requireSuccessfulMessagingDelivery(deliveryResult, 'Final messaging delivery');
|
|
176
230
|
}, {
|
|
177
231
|
...engine.messagingDeliveryRetry,
|
|
178
232
|
label: `MessagingDelivery ${platform}`,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
&& (
|
|
182
|
-
error?.code === 'MESSAGING_DELIVERY_FAILED'
|
|
183
|
-
|| isTransientError(error)
|
|
184
|
-
)
|
|
185
|
-
),
|
|
233
|
+
signal: runMeta.abortController?.signal || null,
|
|
234
|
+
isRetryable: isSafeMessagingDeliveryRetry,
|
|
186
235
|
});
|
|
187
236
|
} catch (error) {
|
|
188
237
|
error.disableAutonomousRetry = true;
|
|
238
|
+
error.deliveredChunks = deliveredChunks.slice();
|
|
189
239
|
throw error;
|
|
190
240
|
}
|
|
241
|
+
deliveredChunks.push(chunks[i]);
|
|
242
|
+
runMeta.lastSentMessage = chunks[i];
|
|
243
|
+
if (!Array.isArray(runMeta.sentMessages)) runMeta.sentMessages = [];
|
|
244
|
+
runMeta.sentMessages.push(chunks[i]);
|
|
191
245
|
}
|
|
192
246
|
|
|
193
|
-
runMeta.lastSentMessage =
|
|
194
|
-
runMeta.sentMessages = Array.isArray(runMeta.sentMessages)
|
|
195
|
-
? [...runMeta.sentMessages, ...chunks]
|
|
196
|
-
: chunks.slice();
|
|
247
|
+
runMeta.lastSentMessage = deliveredChunks[deliveredChunks.length - 1] || cleanedContent;
|
|
197
248
|
engine.markRunFinalDelivery(runId, runMeta.lastSentMessage);
|
|
198
|
-
return { sent: true, chunks };
|
|
249
|
+
return { sent: true, chunks: deliveredChunks };
|
|
199
250
|
}
|
|
200
251
|
|
|
201
252
|
async function tickMessagingProgressSupervisor(engine, runId) {
|
|
@@ -206,54 +257,71 @@ async function tickMessagingProgressSupervisor(engine, runId) {
|
|
|
206
257
|
if (runMeta.terminalInterim) {
|
|
207
258
|
return { sent: false, skipped: true };
|
|
208
259
|
}
|
|
260
|
+
if (runMeta.progressSupervisorTickInFlight === true) {
|
|
261
|
+
return { sent: false, skipped: true, inFlight: true };
|
|
262
|
+
}
|
|
263
|
+
runMeta.progressSupervisorTickInFlight = true;
|
|
264
|
+
let settleTick;
|
|
265
|
+
const tickPromise = new Promise((resolve) => { settleTick = resolve; });
|
|
266
|
+
runMeta.progressSupervisorTickPromise = tickPromise;
|
|
209
267
|
|
|
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',
|
|
268
|
+
try {
|
|
269
|
+
const ledger = runMeta.progressLedger || buildInitialProgressLedger({
|
|
270
|
+
startedAt: runMeta.startedAtIso || isoNow(),
|
|
221
271
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
phase: ledger.currentPhase || 'idle',
|
|
226
|
-
}, { agentId: runMeta.agentId });
|
|
227
|
-
}
|
|
272
|
+
runMeta.progressLedger = ledger;
|
|
273
|
+
const liveness = evaluateProgressLiveness(runMeta);
|
|
274
|
+
const stalled = liveness.stalled;
|
|
228
275
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
276
|
+
if (stalled && !ledger.stallNotifiedAt) {
|
|
277
|
+
engine.updateRunProgress(runId, {
|
|
278
|
+
stallNotifiedAt: isoNow(),
|
|
279
|
+
progressState: 'stalled',
|
|
280
|
+
});
|
|
281
|
+
engine.recordRunEvent(runMeta.userId, runId, 'progress_stalled', {
|
|
282
|
+
currentTool: ledger.currentTool || null,
|
|
283
|
+
currentStep: ledger.currentStep || null,
|
|
284
|
+
phase: ledger.currentPhase || 'idle',
|
|
285
|
+
}, { agentId: runMeta.agentId });
|
|
286
|
+
}
|
|
232
287
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
288
|
+
if (!liveness.shouldNudge) {
|
|
289
|
+
return { sent: false, skipped: true };
|
|
290
|
+
}
|
|
237
291
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
292
|
+
const lastSupervisorNudgeAtMs = timestampMs(runMeta.lastSupervisorNudgeAt, 0);
|
|
293
|
+
if (lastSupervisorNudgeAtMs > 0 && (Date.now() - lastSupervisorNudgeAtMs) < REPEAT_UPDATE_MS) {
|
|
294
|
+
return { sent: false, skipped: true };
|
|
295
|
+
}
|
|
241
296
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
297
|
+
if (ledger.currentPhase === 'tool' || ledger.currentPhase === 'model') {
|
|
298
|
+
return await sendRuntimeMessagingHeartbeat(engine, runId, { stalled });
|
|
299
|
+
}
|
|
245
300
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
301
|
+
if (ledger.currentPhase !== 'idle') {
|
|
302
|
+
return { sent: false, skipped: true };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const queued = engine.enqueueSystemSteering(runId, buildProgressNudge({ stalled }), {
|
|
306
|
+
reason: stalled ? 'stalled_progress_check' : 'progress_check',
|
|
307
|
+
});
|
|
308
|
+
if (!queued) {
|
|
309
|
+
return { sent: false, skipped: true };
|
|
310
|
+
}
|
|
311
|
+
runMeta.lastSupervisorNudgeAt = isoNow();
|
|
312
|
+
engine.updateRunProgress(runId, {
|
|
313
|
+
lastUserVisibleUpdateAt: ledger.lastUserVisibleUpdateAt || null,
|
|
314
|
+
});
|
|
315
|
+
return { sent: false, queued: true };
|
|
316
|
+
} finally {
|
|
317
|
+
settleTick();
|
|
318
|
+
if (engine.getRunMeta(runId) === runMeta) {
|
|
319
|
+
runMeta.progressSupervisorTickInFlight = false;
|
|
320
|
+
if (runMeta.progressSupervisorTickPromise === tickPromise) {
|
|
321
|
+
runMeta.progressSupervisorTickPromise = null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
251
324
|
}
|
|
252
|
-
runMeta.lastSupervisorNudgeAt = isoNow();
|
|
253
|
-
engine.updateRunProgress(runId, {
|
|
254
|
-
lastUserVisibleUpdateAt: ledger.lastUserVisibleUpdateAt || null,
|
|
255
|
-
});
|
|
256
|
-
return { sent: false, queued: true };
|
|
257
325
|
}
|
|
258
326
|
|
|
259
327
|
function startMessagingProgressSupervisor(engine, runId) {
|
|
@@ -264,25 +332,29 @@ function startMessagingProgressSupervisor(engine, runId) {
|
|
|
264
332
|
if (runMeta.messagingProgressSupervisor?.timer) {
|
|
265
333
|
return true;
|
|
266
334
|
}
|
|
335
|
+
const abortController = new AbortController();
|
|
267
336
|
const timer = setInterval(() => {
|
|
268
337
|
tickMessagingProgressSupervisor(engine, runId).catch((error) => {
|
|
269
338
|
console.warn('[Engine] Messaging progress supervisor failed:', error?.message || error);
|
|
270
339
|
});
|
|
271
340
|
}, TICK_MS);
|
|
272
341
|
timer.unref?.();
|
|
273
|
-
runMeta.messagingProgressSupervisor = { timer };
|
|
342
|
+
runMeta.messagingProgressSupervisor = { timer, abortController };
|
|
274
343
|
return true;
|
|
275
344
|
}
|
|
276
345
|
|
|
277
346
|
function stopMessagingProgressSupervisor(engine, runId) {
|
|
278
347
|
const runMeta = engine.getRunMeta(runId);
|
|
348
|
+
const inFlight = runMeta?.progressSupervisorTickPromise || null;
|
|
279
349
|
const timer = runMeta?.messagingProgressSupervisor?.timer || null;
|
|
280
350
|
if (timer) {
|
|
281
351
|
clearInterval(timer);
|
|
282
352
|
}
|
|
353
|
+
runMeta?.messagingProgressSupervisor?.abortController?.abort('Progress supervisor stopped.');
|
|
283
354
|
if (runMeta?.messagingProgressSupervisor) {
|
|
284
355
|
runMeta.messagingProgressSupervisor = null;
|
|
285
356
|
}
|
|
357
|
+
return inFlight || Promise.resolve();
|
|
286
358
|
}
|
|
287
359
|
|
|
288
360
|
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,47 +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
|
-
const error = new Error(`${label} timed out after ${formatElapsedDuration(timeoutMs)}.`);
|
|
38
|
-
error.code = 'MODEL_CALL_TIMEOUT';
|
|
39
|
-
reject(error);
|
|
40
|
-
}, timeoutMs);
|
|
41
|
-
});
|
|
42
|
-
try {
|
|
43
|
-
return await Promise.race([Promise.resolve(promise), timeout]);
|
|
44
|
-
} finally {
|
|
45
|
-
if (timer) clearTimeout(timer);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
18
|
async function requestStructuredJson(engine, {
|
|
50
19
|
provider,
|
|
51
20
|
providerName,
|
|
@@ -61,6 +30,11 @@ async function requestStructuredJson(engine, {
|
|
|
61
30
|
}) {
|
|
62
31
|
const startedAt = Date.now();
|
|
63
32
|
const structuredStep = `model:${phase}`;
|
|
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 });
|
|
64
38
|
if (telemetry?.runId) {
|
|
65
39
|
engine.updateRunProgress(telemetry.runId, {
|
|
66
40
|
currentPhase: 'model',
|
|
@@ -70,7 +44,6 @@ async function requestStructuredJson(engine, {
|
|
|
70
44
|
});
|
|
71
45
|
}
|
|
72
46
|
|
|
73
|
-
let completed = false;
|
|
74
47
|
try {
|
|
75
48
|
const response = await withProviderRetry(
|
|
76
49
|
() => withModelCallTimeout(
|
|
@@ -84,14 +57,18 @@ async function requestStructuredJson(engine, {
|
|
|
84
57
|
model,
|
|
85
58
|
maxTokens,
|
|
86
59
|
reasoningEffort: reasoningEffort || engine.getReasoningEffort(providerName, {}),
|
|
60
|
+
signal: modelAbortController.signal,
|
|
87
61
|
}
|
|
88
62
|
),
|
|
89
|
-
telemetry || {},
|
|
63
|
+
{ ...(telemetry || {}), modelAbortController },
|
|
90
64
|
`${phase} model call`,
|
|
91
65
|
),
|
|
92
|
-
{
|
|
66
|
+
{
|
|
67
|
+
label: `Engine ${model} (structured)`,
|
|
68
|
+
isRetryable: (err) => !modelAbortController.signal.aborted && isTransientError(err),
|
|
69
|
+
signal: modelAbortController.signal,
|
|
70
|
+
}
|
|
93
71
|
);
|
|
94
|
-
completed = true;
|
|
95
72
|
if (telemetry?.runId && telemetry?.userId) {
|
|
96
73
|
recordModelUsage({
|
|
97
74
|
runId: telemetry.runId,
|
|
@@ -114,6 +91,7 @@ async function requestStructuredJson(engine, {
|
|
|
114
91
|
usage: normalizedUsage?.totalTokens || 0,
|
|
115
92
|
};
|
|
116
93
|
} finally {
|
|
94
|
+
parentSignal?.removeEventListener('abort', abortFromParent);
|
|
117
95
|
const runMeta = telemetry?.runId ? engine.getRunMeta(telemetry.runId) : null;
|
|
118
96
|
if (runMeta?.progressLedger?.currentStep === structuredStep) {
|
|
119
97
|
engine.updateRunProgress(telemetry.runId, {
|
|
@@ -121,8 +99,6 @@ async function requestStructuredJson(engine, {
|
|
|
121
99
|
currentStep: null,
|
|
122
100
|
currentTool: null,
|
|
123
101
|
currentStepStartedAt: null,
|
|
124
|
-
}, {
|
|
125
|
-
verified: completed,
|
|
126
102
|
});
|
|
127
103
|
}
|
|
128
104
|
}
|
|
@@ -140,9 +116,15 @@ async function requestModelResponse(engine, {
|
|
|
140
116
|
}) {
|
|
141
117
|
const startedAt = Date.now();
|
|
142
118
|
const requestMessages = sanitizeConversationMessages(messages);
|
|
119
|
+
const modelAbortController = new AbortController();
|
|
120
|
+
const parentSignal = options.signal;
|
|
121
|
+
const abortFromParent = () => modelAbortController.abort(parentSignal?.reason);
|
|
122
|
+
if (parentSignal?.aborted) abortFromParent();
|
|
123
|
+
else parentSignal?.addEventListener('abort', abortFromParent, { once: true });
|
|
143
124
|
const callOptions = {
|
|
144
125
|
model,
|
|
145
126
|
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
127
|
+
signal: modelAbortController.signal,
|
|
146
128
|
};
|
|
147
129
|
|
|
148
130
|
const attemptModelCall = async () => {
|
|
@@ -157,7 +139,7 @@ async function requestModelResponse(engine, {
|
|
|
157
139
|
while (true) {
|
|
158
140
|
const next = await withModelCallTimeout(
|
|
159
141
|
iterator.next(),
|
|
160
|
-
options,
|
|
142
|
+
{ ...options, modelAbortController },
|
|
161
143
|
`Model stream iteration ${iteration}`,
|
|
162
144
|
);
|
|
163
145
|
if (next.done) break;
|
|
@@ -194,7 +176,7 @@ async function requestModelResponse(engine, {
|
|
|
194
176
|
} else {
|
|
195
177
|
response = await withModelCallTimeout(
|
|
196
178
|
provider.chat(requestMessages, tools, callOptions),
|
|
197
|
-
options,
|
|
179
|
+
{ ...options, modelAbortController },
|
|
198
180
|
`Model iteration ${iteration}`,
|
|
199
181
|
);
|
|
200
182
|
}
|
|
@@ -202,18 +184,27 @@ async function requestModelResponse(engine, {
|
|
|
202
184
|
return { response, streamContent };
|
|
203
185
|
};
|
|
204
186
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
|
|
187
|
+
let response;
|
|
188
|
+
let streamContent;
|
|
189
|
+
try {
|
|
190
|
+
({ response, streamContent } = await withProviderRetry(attemptModelCall, {
|
|
191
|
+
...(options.retry || {}),
|
|
192
|
+
label: `Engine ${model}`,
|
|
193
|
+
isRetryable: (err) => !modelAbortController.signal.aborted
|
|
194
|
+
&& !err?.__providerRetryUnsafe
|
|
195
|
+
&& isTransientError(err),
|
|
196
|
+
onRetry: ({ attempt, delayMs }) => {
|
|
197
|
+
engine.emit(options.userId, 'run:interim', {
|
|
198
|
+
runId,
|
|
199
|
+
message: `Model service busy; retrying (attempt ${attempt}) in ${Math.max(1, Math.round(delayMs / 1000))}s.`,
|
|
200
|
+
phase: 'recovering',
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
signal: modelAbortController.signal,
|
|
204
|
+
}));
|
|
205
|
+
} finally {
|
|
206
|
+
parentSignal?.removeEventListener('abort', abortFromParent);
|
|
207
|
+
}
|
|
217
208
|
|
|
218
209
|
const resolvedResponse = response || {
|
|
219
210
|
content: streamContent,
|
|
@@ -254,5 +245,6 @@ module.exports = {
|
|
|
254
245
|
requestModelResponse,
|
|
255
246
|
requestStructuredJson,
|
|
256
247
|
resolveModelCallTimeoutMs,
|
|
248
|
+
runAbortableModelCall,
|
|
257
249
|
withModelCallTimeout,
|
|
258
250
|
};
|