neoagent 2.5.2-beta.0 → 2.5.2-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/flutter_app/lib/main_chat.dart +0 -3
- package/flutter_app/lib/main_shared.dart +40 -29
- package/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +19016 -19016
- package/server/services/ai/deliverables/artifact_helpers.js +19 -9
- package/server/services/ai/engine.js +2 -3813
- package/server/services/ai/loop/agent_engine_core.js +1590 -0
- package/server/services/ai/loop/callbacks.js +151 -0
- package/server/services/ai/loop/completion_judge.js +252 -0
- package/server/services/ai/loop/conversation_loop.js +2343 -0
- package/server/services/ai/loop/delivery_state.js +27 -0
- package/server/services/ai/loop/error_recovery.js +49 -0
- package/server/services/ai/loop/iteration_budget.js +24 -0
- package/server/services/ai/loop/messaging_delivery.js +250 -0
- package/server/services/ai/loop/model_io.js +258 -0
- package/server/services/ai/loop/progress_monitor.js +80 -0
- package/server/services/ai/loop/run_state.js +356 -0
- package/server/services/ai/loop/tool_dispatch.js +230 -0
- package/server/services/ai/tools.js +67 -17
- package/server/services/messaging/manager.js +7 -0
- package/server/services/runtime/backends/local-vm.js +7 -7
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createDeliveryState(seed = {}) {
|
|
4
|
+
return {
|
|
5
|
+
alreadySent: seed.alreadySent === true,
|
|
6
|
+
finalResponseSent: seed.finalResponseSent === true,
|
|
7
|
+
finalContentDelivered: seed.finalContentDelivered === true,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function markInterimDelivered(state) {
|
|
12
|
+
if (!state) return;
|
|
13
|
+
state.alreadySent = true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function markFinalDelivered(state) {
|
|
17
|
+
if (!state) return;
|
|
18
|
+
state.alreadySent = true;
|
|
19
|
+
state.finalResponseSent = true;
|
|
20
|
+
state.finalContentDelivered = true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
createDeliveryState,
|
|
25
|
+
markInterimDelivered,
|
|
26
|
+
markFinalDelivered,
|
|
27
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function hasTerminalMessagingDelivery(runMeta = null) {
|
|
4
|
+
return runMeta?.finalDeliverySent === true
|
|
5
|
+
|| runMeta?.finalResponseSent === true
|
|
6
|
+
|| runMeta?.finalContentDelivered === true
|
|
7
|
+
|| runMeta?.deliveryState?.finalResponseSent === true
|
|
8
|
+
|| runMeta?.deliveryState?.finalContentDelivered === true
|
|
9
|
+
|| runMeta?.noResponse === true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isRateLimitError(error = null) {
|
|
13
|
+
return /429|rate.?limit|free-models-per/i.test(String(error?.message || ''));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function shouldRetryMessagingRun({
|
|
17
|
+
triggerSource,
|
|
18
|
+
options = {},
|
|
19
|
+
runMeta = null,
|
|
20
|
+
error = null,
|
|
21
|
+
retryCount = 0,
|
|
22
|
+
retryLimit = 0,
|
|
23
|
+
} = {}) {
|
|
24
|
+
return triggerSource === 'messaging'
|
|
25
|
+
&& Boolean(options.source)
|
|
26
|
+
&& Boolean(options.chatId)
|
|
27
|
+
&& !hasTerminalMessagingDelivery(runMeta)
|
|
28
|
+
&& error?.disableAutonomousRetry !== true
|
|
29
|
+
&& !isRateLimitError(error)
|
|
30
|
+
&& Number(retryCount || 0) < Number(retryLimit || 0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function shouldSendMessagingErrorFallback({
|
|
34
|
+
triggerSource,
|
|
35
|
+
options = {},
|
|
36
|
+
runMeta = null,
|
|
37
|
+
} = {}) {
|
|
38
|
+
return triggerSource === 'messaging'
|
|
39
|
+
&& Boolean(options.source)
|
|
40
|
+
&& Boolean(options.chatId)
|
|
41
|
+
&& !hasTerminalMessagingDelivery(runMeta);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
hasTerminalMessagingDelivery,
|
|
46
|
+
isRateLimitError,
|
|
47
|
+
shouldRetryMessagingRun,
|
|
48
|
+
shouldSendMessagingErrorFallback,
|
|
49
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class IterationBudget {
|
|
4
|
+
constructor(maxTotal) {
|
|
5
|
+
this.maxTotal = Math.max(0, Number(maxTotal) || 0);
|
|
6
|
+
this.used = 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
consume() {
|
|
10
|
+
if (this.used >= this.maxTotal) return false;
|
|
11
|
+
this.used += 1;
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
refund() {
|
|
16
|
+
if (this.used > 0) this.used -= 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get remaining() {
|
|
20
|
+
return Math.max(0, this.maxTotal - this.used);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { IterationBudget };
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
splitOutgoingMessageForPlatform,
|
|
5
|
+
} = require('../../messaging/formatting_guides');
|
|
6
|
+
const {
|
|
7
|
+
normalizeOutgoingMessage,
|
|
8
|
+
} = require('../messagingFallback');
|
|
9
|
+
const { withProviderRetry, isTransientError } = require('../providerRetry');
|
|
10
|
+
const { shortenRunId, summarizeForLog } = require('../logFormat');
|
|
11
|
+
const {
|
|
12
|
+
TICK_MS,
|
|
13
|
+
REPEAT_UPDATE_MS,
|
|
14
|
+
buildInitialProgressLedger,
|
|
15
|
+
buildProgressNudge,
|
|
16
|
+
evaluateProgressLiveness,
|
|
17
|
+
} = require('./progress_monitor');
|
|
18
|
+
|
|
19
|
+
function isoNow() {
|
|
20
|
+
return new Date().toISOString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function timestampMs(value, fallback = 0) {
|
|
24
|
+
const resolved = value ? Date.parse(value) : NaN;
|
|
25
|
+
return Number.isFinite(resolved) ? resolved : fallback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function requireSuccessfulMessagingDelivery(result, label = 'Messaging delivery') {
|
|
29
|
+
if (result?.success === true && result?.suppressed !== true) {
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
const reason = String(
|
|
33
|
+
result?.error
|
|
34
|
+
|| result?.reason
|
|
35
|
+
|| result?.result?.error
|
|
36
|
+
|| result?.result?.reason
|
|
37
|
+
|| 'the platform did not confirm delivery',
|
|
38
|
+
).trim();
|
|
39
|
+
const error = new Error(`${label} failed: ${reason}`);
|
|
40
|
+
error.code = 'MESSAGING_DELIVERY_FAILED';
|
|
41
|
+
error.deliveryResult = result || null;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function sendRuntimeMessagingHeartbeat(engine, runId, options = {}) {
|
|
46
|
+
const runMeta = engine.getRunMeta(runId);
|
|
47
|
+
if (!runMeta || runMeta.aborted) return { sent: false, skipped: true };
|
|
48
|
+
if (runMeta.triggerSource !== 'messaging') {
|
|
49
|
+
return { sent: false, skipped: true };
|
|
50
|
+
}
|
|
51
|
+
const createdAt = isoNow();
|
|
52
|
+
const heartbeatCount = Number(runMeta.progressLedger?.heartbeatCount || 0) + 1;
|
|
53
|
+
runMeta.lastSupervisorNudgeAt = createdAt;
|
|
54
|
+
engine.updateRunProgress(runId, {
|
|
55
|
+
heartbeatCount,
|
|
56
|
+
});
|
|
57
|
+
engine.recordRunEvent(runMeta.userId, runId, 'progress_heartbeat_sent', {
|
|
58
|
+
stalled: options.stalled === true,
|
|
59
|
+
currentTool: runMeta.progressLedger?.currentTool || null,
|
|
60
|
+
currentStep: runMeta.progressLedger?.currentStep || null,
|
|
61
|
+
phase: runMeta.progressLedger?.currentPhase || 'idle',
|
|
62
|
+
userVisible: false,
|
|
63
|
+
createdAt,
|
|
64
|
+
}, { agentId: runMeta.agentId });
|
|
65
|
+
engine.enqueueSystemSteering(
|
|
66
|
+
runId,
|
|
67
|
+
buildProgressNudge({ stalled: options.stalled === true }),
|
|
68
|
+
{ reason: options.stalled === true ? 'stalled_progress_check' : 'progress_check' },
|
|
69
|
+
);
|
|
70
|
+
return { sent: false, heartbeat: true, queued: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function shouldSendMessagingFinalFallback(_engine, runMeta, content, platform = null) {
|
|
74
|
+
const cleanedContent = normalizeOutgoingMessage(content || '', platform, {
|
|
75
|
+
collapseWhitespace: false,
|
|
76
|
+
});
|
|
77
|
+
const lastFinalDeliveryMessage = normalizeOutgoingMessage(
|
|
78
|
+
runMeta?.lastSentMessage
|
|
79
|
+
|| (Array.isArray(runMeta?.sentMessages) ? runMeta.sentMessages[runMeta.sentMessages.length - 1] : '')
|
|
80
|
+
|| '',
|
|
81
|
+
platform,
|
|
82
|
+
);
|
|
83
|
+
return Boolean(
|
|
84
|
+
cleanedContent
|
|
85
|
+
&& !runMeta?.terminalInterim
|
|
86
|
+
&& runMeta?.explicitMessageSent !== true
|
|
87
|
+
&& runMeta?.finalDeliverySent !== true
|
|
88
|
+
&& runMeta?.deliveryState?.finalContentDelivered !== true
|
|
89
|
+
&& !lastFinalDeliveryMessage
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function deliverMessagingFinalFallback(engine, {
|
|
94
|
+
runId,
|
|
95
|
+
userId,
|
|
96
|
+
agentId,
|
|
97
|
+
platform,
|
|
98
|
+
chatId,
|
|
99
|
+
content,
|
|
100
|
+
}) {
|
|
101
|
+
const runMeta = engine.getRunMeta(runId);
|
|
102
|
+
if (!runMeta || !engine.messagingManager) return { sent: false, skipped: true };
|
|
103
|
+
const cleanedContent = normalizeOutgoingMessage(content || '', platform, {
|
|
104
|
+
collapseWhitespace: false,
|
|
105
|
+
});
|
|
106
|
+
if (!shouldSendMessagingFinalFallback(engine, runMeta, cleanedContent, platform)) {
|
|
107
|
+
return { sent: false, skipped: true };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const chunks = splitOutgoingMessageForPlatform(platform, cleanedContent);
|
|
111
|
+
console.info(
|
|
112
|
+
`[Run ${shortenRunId(runId)}] messaging_fallback chunks=${chunks.length} to=${summarizeForLog(chatId, 80)}`
|
|
113
|
+
);
|
|
114
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
115
|
+
if (i > 0) {
|
|
116
|
+
const delay = Math.max(1000, Math.min(chunks[i].length * 30, 4000));
|
|
117
|
+
await engine.messagingManager.sendTyping(userId, platform, chatId, true, { agentId }).catch(() => {});
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
await withProviderRetry(async () => {
|
|
122
|
+
const deliveryResult = await engine.messagingManager.sendMessage(
|
|
123
|
+
userId,
|
|
124
|
+
platform,
|
|
125
|
+
chatId,
|
|
126
|
+
chunks[i],
|
|
127
|
+
{ runId, agentId },
|
|
128
|
+
);
|
|
129
|
+
return requireSuccessfulMessagingDelivery(deliveryResult, 'Final messaging delivery');
|
|
130
|
+
}, {
|
|
131
|
+
...engine.messagingDeliveryRetry,
|
|
132
|
+
label: `MessagingDelivery ${platform}`,
|
|
133
|
+
isRetryable: (error) => (
|
|
134
|
+
error?.retryable !== false
|
|
135
|
+
&& (
|
|
136
|
+
error?.code === 'MESSAGING_DELIVERY_FAILED'
|
|
137
|
+
|| isTransientError(error)
|
|
138
|
+
)
|
|
139
|
+
),
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
error.disableAutonomousRetry = true;
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
runMeta.lastSentMessage = chunks[chunks.length - 1] || cleanedContent;
|
|
148
|
+
runMeta.sentMessages = Array.isArray(runMeta.sentMessages)
|
|
149
|
+
? [...runMeta.sentMessages, ...chunks]
|
|
150
|
+
: chunks.slice();
|
|
151
|
+
engine.markRunFinalDelivery(runId, runMeta.lastSentMessage);
|
|
152
|
+
return { sent: true, chunks };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function tickMessagingProgressSupervisor(engine, runId) {
|
|
156
|
+
const runMeta = engine.getRunMeta(runId);
|
|
157
|
+
if (!runMeta || runMeta.aborted || runMeta.triggerSource !== 'messaging') {
|
|
158
|
+
return { sent: false, skipped: true };
|
|
159
|
+
}
|
|
160
|
+
if (runMeta.terminalInterim) {
|
|
161
|
+
return { sent: false, skipped: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const ledger = runMeta.progressLedger || buildInitialProgressLedger({
|
|
165
|
+
startedAt: runMeta.startedAtIso || isoNow(),
|
|
166
|
+
});
|
|
167
|
+
runMeta.progressLedger = ledger;
|
|
168
|
+
const liveness = evaluateProgressLiveness(runMeta);
|
|
169
|
+
const stalled = liveness.stalled;
|
|
170
|
+
|
|
171
|
+
if (stalled && !ledger.stallNotifiedAt) {
|
|
172
|
+
engine.updateRunProgress(runId, {
|
|
173
|
+
stallNotifiedAt: isoNow(),
|
|
174
|
+
progressState: 'stalled',
|
|
175
|
+
});
|
|
176
|
+
engine.recordRunEvent(runMeta.userId, runId, 'progress_stalled', {
|
|
177
|
+
currentTool: ledger.currentTool || null,
|
|
178
|
+
currentStep: ledger.currentStep || null,
|
|
179
|
+
phase: ledger.currentPhase || 'idle',
|
|
180
|
+
}, { agentId: runMeta.agentId });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!liveness.shouldNudge) {
|
|
184
|
+
return { sent: false, skipped: true };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const lastSupervisorNudgeAtMs = timestampMs(runMeta.lastSupervisorNudgeAt, 0);
|
|
188
|
+
if (lastSupervisorNudgeAtMs > 0 && (Date.now() - lastSupervisorNudgeAtMs) < REPEAT_UPDATE_MS) {
|
|
189
|
+
return { sent: false, skipped: true };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (ledger.currentPhase === 'tool' || ledger.currentPhase === 'model') {
|
|
193
|
+
return sendRuntimeMessagingHeartbeat(engine, runId, { stalled });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (ledger.currentPhase !== 'idle') {
|
|
197
|
+
return { sent: false, skipped: true };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const queued = engine.enqueueSystemSteering(runId, buildProgressNudge({ stalled }), {
|
|
201
|
+
reason: stalled ? 'stalled_progress_check' : 'progress_check',
|
|
202
|
+
});
|
|
203
|
+
if (!queued) {
|
|
204
|
+
return { sent: false, skipped: true };
|
|
205
|
+
}
|
|
206
|
+
runMeta.lastSupervisorNudgeAt = isoNow();
|
|
207
|
+
engine.updateRunProgress(runId, {
|
|
208
|
+
lastUserVisibleUpdateAt: ledger.lastUserVisibleUpdateAt || null,
|
|
209
|
+
});
|
|
210
|
+
return { sent: false, queued: true };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function startMessagingProgressSupervisor(engine, runId) {
|
|
214
|
+
const runMeta = engine.getRunMeta(runId);
|
|
215
|
+
if (!runMeta || runMeta.triggerSource !== 'messaging' || !runMeta.messagingContext?.platform || !runMeta.messagingContext?.chatId) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
if (runMeta.messagingProgressSupervisor?.timer) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
const timer = setInterval(() => {
|
|
222
|
+
tickMessagingProgressSupervisor(engine, runId).catch((error) => {
|
|
223
|
+
console.warn('[Engine] Messaging progress supervisor failed:', error?.message || error);
|
|
224
|
+
});
|
|
225
|
+
}, TICK_MS);
|
|
226
|
+
timer.unref?.();
|
|
227
|
+
runMeta.messagingProgressSupervisor = { timer };
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function stopMessagingProgressSupervisor(engine, runId) {
|
|
232
|
+
const runMeta = engine.getRunMeta(runId);
|
|
233
|
+
const timer = runMeta?.messagingProgressSupervisor?.timer || null;
|
|
234
|
+
if (timer) {
|
|
235
|
+
clearInterval(timer);
|
|
236
|
+
}
|
|
237
|
+
if (runMeta?.messagingProgressSupervisor) {
|
|
238
|
+
runMeta.messagingProgressSupervisor = null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
deliverMessagingFinalFallback,
|
|
244
|
+
requireSuccessfulMessagingDelivery,
|
|
245
|
+
sendRuntimeMessagingHeartbeat,
|
|
246
|
+
shouldSendMessagingFinalFallback,
|
|
247
|
+
startMessagingProgressSupervisor,
|
|
248
|
+
stopMessagingProgressSupervisor,
|
|
249
|
+
tickMessagingProgressSupervisor,
|
|
250
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { sanitizeConversationMessages } = require('../history');
|
|
4
|
+
const { sanitizeModelOutput } = require('../outputSanitizer');
|
|
5
|
+
const { parseJsonObject } = require('../taskAnalysis');
|
|
6
|
+
const { withProviderRetry, isTransientError } = require('../providerRetry');
|
|
7
|
+
const { normalizeUsage, recordModelUsage } = require('../usage');
|
|
8
|
+
|
|
9
|
+
const MODEL_CALL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
function isoNow() {
|
|
12
|
+
return new Date().toISOString();
|
|
13
|
+
}
|
|
14
|
+
|
|
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
|
+
async function requestStructuredJson(engine, {
|
|
50
|
+
provider,
|
|
51
|
+
providerName,
|
|
52
|
+
model,
|
|
53
|
+
messages,
|
|
54
|
+
prompt,
|
|
55
|
+
maxTokens = 1400,
|
|
56
|
+
normalize,
|
|
57
|
+
fallback = {},
|
|
58
|
+
reasoningEffort,
|
|
59
|
+
telemetry = null,
|
|
60
|
+
phase = 'structured',
|
|
61
|
+
}) {
|
|
62
|
+
const startedAt = Date.now();
|
|
63
|
+
const structuredStep = `model:${phase}`;
|
|
64
|
+
if (telemetry?.runId) {
|
|
65
|
+
engine.updateRunProgress(telemetry.runId, {
|
|
66
|
+
currentPhase: 'model',
|
|
67
|
+
currentStep: structuredStep,
|
|
68
|
+
currentTool: null,
|
|
69
|
+
currentStepStartedAt: isoNow(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let completed = false;
|
|
74
|
+
try {
|
|
75
|
+
const response = await withProviderRetry(
|
|
76
|
+
() => withModelCallTimeout(
|
|
77
|
+
provider.chat(
|
|
78
|
+
sanitizeConversationMessages([
|
|
79
|
+
...messages,
|
|
80
|
+
{ role: 'system', content: prompt },
|
|
81
|
+
]),
|
|
82
|
+
[],
|
|
83
|
+
{
|
|
84
|
+
model,
|
|
85
|
+
maxTokens,
|
|
86
|
+
reasoningEffort: reasoningEffort || engine.getReasoningEffort(providerName, {}),
|
|
87
|
+
}
|
|
88
|
+
),
|
|
89
|
+
telemetry || {},
|
|
90
|
+
`${phase} model call`,
|
|
91
|
+
),
|
|
92
|
+
{ label: `Engine ${model} (structured)` }
|
|
93
|
+
);
|
|
94
|
+
completed = true;
|
|
95
|
+
if (telemetry?.runId && telemetry?.userId) {
|
|
96
|
+
recordModelUsage({
|
|
97
|
+
runId: telemetry.runId,
|
|
98
|
+
stepId: telemetry.stepId || null,
|
|
99
|
+
userId: telemetry.userId,
|
|
100
|
+
agentId: telemetry.agentId || null,
|
|
101
|
+
provider: providerName,
|
|
102
|
+
model,
|
|
103
|
+
phase,
|
|
104
|
+
usage: response.usage,
|
|
105
|
+
latencyMs: Date.now() - startedAt,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const parsed = parseJsonObject(response.content || '');
|
|
110
|
+
const normalizedUsage = normalizeUsage(response.usage);
|
|
111
|
+
return {
|
|
112
|
+
value: normalize(parsed || {}, fallback),
|
|
113
|
+
raw: response.content || '',
|
|
114
|
+
usage: normalizedUsage?.totalTokens || 0,
|
|
115
|
+
};
|
|
116
|
+
} finally {
|
|
117
|
+
const runMeta = telemetry?.runId ? engine.getRunMeta(telemetry.runId) : null;
|
|
118
|
+
if (runMeta?.progressLedger?.currentStep === structuredStep) {
|
|
119
|
+
engine.updateRunProgress(telemetry.runId, {
|
|
120
|
+
currentPhase: 'idle',
|
|
121
|
+
currentStep: null,
|
|
122
|
+
currentTool: null,
|
|
123
|
+
currentStepStartedAt: null,
|
|
124
|
+
}, {
|
|
125
|
+
verified: completed,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function requestModelResponse(engine, {
|
|
132
|
+
provider,
|
|
133
|
+
providerName,
|
|
134
|
+
model,
|
|
135
|
+
messages,
|
|
136
|
+
tools,
|
|
137
|
+
options,
|
|
138
|
+
runId,
|
|
139
|
+
iteration,
|
|
140
|
+
}) {
|
|
141
|
+
const startedAt = Date.now();
|
|
142
|
+
const requestMessages = sanitizeConversationMessages(messages);
|
|
143
|
+
const callOptions = {
|
|
144
|
+
model,
|
|
145
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const attemptModelCall = async () => {
|
|
149
|
+
let response = null;
|
|
150
|
+
let streamContent = '';
|
|
151
|
+
|
|
152
|
+
if (options.stream !== false) {
|
|
153
|
+
let emittedContent = false;
|
|
154
|
+
const stream = provider.stream(requestMessages, tools, callOptions);
|
|
155
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
156
|
+
try {
|
|
157
|
+
while (true) {
|
|
158
|
+
const next = await withModelCallTimeout(
|
|
159
|
+
iterator.next(),
|
|
160
|
+
options,
|
|
161
|
+
`Model stream iteration ${iteration}`,
|
|
162
|
+
);
|
|
163
|
+
if (next.done) break;
|
|
164
|
+
const chunk = next.value;
|
|
165
|
+
if (chunk.type === 'content') {
|
|
166
|
+
emittedContent = true;
|
|
167
|
+
streamContent += chunk.content;
|
|
168
|
+
engine.emit(options.userId, 'run:stream', {
|
|
169
|
+
runId,
|
|
170
|
+
content: sanitizeModelOutput(streamContent, { model }),
|
|
171
|
+
iteration,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (chunk.type === 'done') {
|
|
175
|
+
response = chunk;
|
|
176
|
+
}
|
|
177
|
+
if (chunk.type === 'tool_calls') {
|
|
178
|
+
response = {
|
|
179
|
+
content: chunk.content || streamContent,
|
|
180
|
+
toolCalls: chunk.toolCalls,
|
|
181
|
+
providerContentBlocks: chunk.providerContentBlocks || null,
|
|
182
|
+
finishReason: 'tool_calls',
|
|
183
|
+
usage: chunk.usage || null,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
Promise.resolve(iterator.return?.()).catch(() => {});
|
|
189
|
+
// Once tokens have streamed to the client a retry would duplicate
|
|
190
|
+
// output, so only the pre-stream window is safe to replay.
|
|
191
|
+
if (emittedContent) err.__providerRetryUnsafe = true;
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
response = await withModelCallTimeout(
|
|
196
|
+
provider.chat(requestMessages, tools, callOptions),
|
|
197
|
+
options,
|
|
198
|
+
`Model iteration ${iteration}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { response, streamContent };
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const { response, streamContent } = await withProviderRetry(attemptModelCall, {
|
|
206
|
+
...(options.retry || {}),
|
|
207
|
+
label: `Engine ${model}`,
|
|
208
|
+
isRetryable: (err) => !err?.__providerRetryUnsafe && isTransientError(err),
|
|
209
|
+
onRetry: ({ attempt, delayMs }) => {
|
|
210
|
+
engine.emit(options.userId, 'run:interim', {
|
|
211
|
+
runId,
|
|
212
|
+
message: `Model service busy; retrying (attempt ${attempt}) in ${Math.max(1, Math.round(delayMs / 1000))}s.`,
|
|
213
|
+
phase: 'recovering',
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const resolvedResponse = response || {
|
|
219
|
+
content: streamContent,
|
|
220
|
+
toolCalls: [],
|
|
221
|
+
finishReason: 'stop',
|
|
222
|
+
usage: null,
|
|
223
|
+
};
|
|
224
|
+
const hasContent = Boolean(String(resolvedResponse.content || streamContent || '').trim());
|
|
225
|
+
const hasToolCalls = Array.isArray(resolvedResponse.toolCalls) && resolvedResponse.toolCalls.length > 0;
|
|
226
|
+
if (!hasContent && !hasToolCalls) {
|
|
227
|
+
const error = new Error(`Model ${model} returned an empty response.`);
|
|
228
|
+
error.code = 'MODEL_EMPTY_RESPONSE';
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
if (options.runId && options.userId) {
|
|
232
|
+
recordModelUsage({
|
|
233
|
+
runId: options.runId,
|
|
234
|
+
stepId: options.stepId || null,
|
|
235
|
+
userId: options.userId,
|
|
236
|
+
agentId: options.agentId || null,
|
|
237
|
+
provider: providerName,
|
|
238
|
+
model,
|
|
239
|
+
phase: options.phase || 'model_turn',
|
|
240
|
+
usage: resolvedResponse.usage,
|
|
241
|
+
latencyMs: Date.now() - startedAt,
|
|
242
|
+
metadata: { iteration },
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
response: resolvedResponse,
|
|
248
|
+
responseModel: model,
|
|
249
|
+
streamContent,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
requestModelResponse,
|
|
255
|
+
requestStructuredJson,
|
|
256
|
+
resolveModelCallTimeoutMs,
|
|
257
|
+
withModelCallTimeout,
|
|
258
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const FIRST_UPDATE_MS = 60 * 1000;
|
|
4
|
+
const REPEAT_UPDATE_MS = 90 * 1000;
|
|
5
|
+
const STALL_MS = 240 * 1000;
|
|
6
|
+
const TICK_MS = 15 * 1000;
|
|
7
|
+
|
|
8
|
+
function isoNow() {
|
|
9
|
+
return new Date().toISOString();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function timestampMs(value, fallback = 0) {
|
|
13
|
+
const resolved = value ? Date.parse(value) : NaN;
|
|
14
|
+
return Number.isFinite(resolved) ? resolved : fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildInitialProgressLedger({ startedAt, retryState = {} } = {}) {
|
|
18
|
+
const startedAtIso = startedAt || isoNow();
|
|
19
|
+
const interimHistory = Array.isArray(retryState.interimHistory)
|
|
20
|
+
? retryState.interimHistory
|
|
21
|
+
.map((item) => String(item?.content || '').trim())
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
: [];
|
|
24
|
+
const lastInterimMessage = interimHistory[interimHistory.length - 1] || '';
|
|
25
|
+
const lastVisibleAt = retryState.lastUserVisibleUpdateAt || (lastInterimMessage ? startedAtIso : null);
|
|
26
|
+
return {
|
|
27
|
+
currentStep: retryState.currentStep || null,
|
|
28
|
+
currentTool: retryState.currentTool || null,
|
|
29
|
+
currentStepStartedAt: retryState.currentStepStartedAt || null,
|
|
30
|
+
lastVerifiedProgressAt: retryState.lastVerifiedProgressAt || startedAtIso,
|
|
31
|
+
lastUserVisibleUpdateAt: lastVisibleAt,
|
|
32
|
+
lastFinalDeliveryAt: retryState.lastFinalDeliveryAt || null,
|
|
33
|
+
heartbeatCount: Number(retryState.heartbeatCount || 0),
|
|
34
|
+
stallNotifiedAt: retryState.stallNotifiedAt || null,
|
|
35
|
+
progressState: retryState.progressState || 'active',
|
|
36
|
+
currentPhase: retryState.currentPhase || 'idle',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function evaluateProgressLiveness(runMeta, now = Date.now()) {
|
|
41
|
+
const startedAtMs = Number.isFinite(runMeta?.startedAt) ? runMeta.startedAt : now;
|
|
42
|
+
const ledger = runMeta?.progressLedger || {};
|
|
43
|
+
const lastVerifiedAtMs = timestampMs(ledger.lastVerifiedProgressAt, startedAtMs);
|
|
44
|
+
const lastVisibleAtMs = timestampMs(ledger.lastUserVisibleUpdateAt, 0);
|
|
45
|
+
const thresholdMs = lastVisibleAtMs > 0 ? REPEAT_UPDATE_MS : FIRST_UPDATE_MS;
|
|
46
|
+
const comparisonVisibleAtMs = lastVisibleAtMs > 0 ? lastVisibleAtMs : startedAtMs;
|
|
47
|
+
const shouldNudge = (now - comparisonVisibleAtMs) >= thresholdMs;
|
|
48
|
+
const stalled = (now - lastVerifiedAtMs) >= STALL_MS;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
startedAtMs,
|
|
52
|
+
thresholdMs,
|
|
53
|
+
shouldNudge,
|
|
54
|
+
stalled,
|
|
55
|
+
phase: ledger.currentPhase || 'idle',
|
|
56
|
+
currentStep: ledger.currentStep || null,
|
|
57
|
+
currentTool: ledger.currentTool || null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildProgressNudge({ stalled = false } = {}) {
|
|
62
|
+
return [
|
|
63
|
+
'Internal progress check for the active messaging run.',
|
|
64
|
+
stalled
|
|
65
|
+
? 'No verified progress has been recorded for the stall threshold.'
|
|
66
|
+
: 'The originating chat has not received a user-visible update for the progress threshold.',
|
|
67
|
+
'On the next normal agent turn, decide whether to continue silently, send a concise model-authored interim update with send_interim_update, report a real blocker, or finish with the final answer.',
|
|
68
|
+
'Do not repeat previous status text and do not treat an interim update as final delivery.',
|
|
69
|
+
].join(' ');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
FIRST_UPDATE_MS,
|
|
74
|
+
REPEAT_UPDATE_MS,
|
|
75
|
+
STALL_MS,
|
|
76
|
+
TICK_MS,
|
|
77
|
+
buildInitialProgressLedger,
|
|
78
|
+
evaluateProgressLiveness,
|
|
79
|
+
buildProgressNudge,
|
|
80
|
+
};
|