@debugg-ai/debugg-ai-mcp 1.0.40 → 1.0.42
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.
|
@@ -9,6 +9,7 @@ import { handleExternalServiceError } from '../utils/errors.js';
|
|
|
9
9
|
import { fetchImageAsBase64, imageContentBlock } from '../utils/imageUtils.js';
|
|
10
10
|
import { DebuggAIServerClient } from '../services/index.js';
|
|
11
11
|
import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
|
|
12
|
+
import { tunnelManager } from '../services/ngrok/tunnelManager.js';
|
|
12
13
|
const logger = new Logger({ module: 'testPageChangesHandler' });
|
|
13
14
|
// Cache the template UUID and project UUIDs within a server session to avoid re-fetching
|
|
14
15
|
let cachedTemplateUuid = null;
|
|
@@ -168,10 +169,10 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
168
169
|
if (stepsTaken > 0) {
|
|
169
170
|
// Extract the latest brain.step to show what the agent is doing
|
|
170
171
|
const latestStep = (exec.nodeExecutions ?? [])
|
|
171
|
-
.filter(n => n.nodeType === 'brain.step' && n.outputData
|
|
172
|
+
.filter(n => n.nodeType === 'brain.step' && n.outputData)
|
|
172
173
|
.sort((a, b) => b.executionOrder - a.executionOrder)[0];
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
const d = latestStep?.outputData?.decision ?? latestStep?.outputData;
|
|
175
|
+
if (d) {
|
|
175
176
|
const action = d.actionType ?? d.action_type ?? 'working';
|
|
176
177
|
const intent = d.intent;
|
|
177
178
|
message = intent
|
|
@@ -196,15 +197,27 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
196
197
|
}
|
|
197
198
|
}, abortController.signal);
|
|
198
199
|
const duration = Date.now() - startTime;
|
|
200
|
+
// If the execution failed because the tunnel URL was unreachable, evict the dead tunnel
|
|
201
|
+
// so the next call re-provisions a fresh one instead of reusing a dead entry.
|
|
202
|
+
const tunnelErrorMsg = finalExecution.errorMessage ?? finalExecution.state?.error ?? '';
|
|
203
|
+
if (ctx.tunnelId && tunnelErrorMsg.includes('unreachable') && tunnelErrorMsg.includes('ngrok')) {
|
|
204
|
+
logger.warn(`Tunnel ${ctx.tunnelId} appears dead (unreachable) — evicting from cache`);
|
|
205
|
+
tunnelManager.stopTunnel(ctx.tunnelId).catch(() => { });
|
|
206
|
+
ctx = { ...ctx, tunnelId: undefined };
|
|
207
|
+
}
|
|
199
208
|
// --- Format result ---
|
|
200
209
|
const outcome = finalExecution.state?.outcome ?? finalExecution.status;
|
|
201
210
|
const nodes = finalExecution.nodeExecutions ?? [];
|
|
202
|
-
//
|
|
211
|
+
// subworkflow.run is the current graph shape — carries outcome, actionHistory, screenshot
|
|
212
|
+
const subworkflowNode = nodes.find(n => n.nodeType === 'subworkflow.run');
|
|
213
|
+
// surfer.execute_task and brain.step/brain.evaluate are older graph shapes
|
|
214
|
+
const surferNode = nodes.find(n => n.nodeType === 'surfer.execute_task');
|
|
215
|
+
// Action trace: brain.step nodes (old) → subworkflow.run actionHistory (new)
|
|
203
216
|
const brainSteps = nodes
|
|
204
|
-
.filter(n => n.nodeType === 'brain.step' && n.outputData
|
|
217
|
+
.filter(n => n.nodeType === 'brain.step' && n.outputData)
|
|
205
218
|
.sort((a, b) => a.executionOrder - b.executionOrder);
|
|
206
219
|
const actionTrace = brainSteps.map((n, i) => {
|
|
207
|
-
const d = n.outputData.decision;
|
|
220
|
+
const d = n.outputData.decision ?? n.outputData;
|
|
208
221
|
return {
|
|
209
222
|
step: i + 1,
|
|
210
223
|
action: d.actionType ?? d.action_type,
|
|
@@ -215,33 +228,52 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
215
228
|
durationMs: n.executionTimeMs,
|
|
216
229
|
};
|
|
217
230
|
});
|
|
218
|
-
|
|
231
|
+
const subworkflowHistory = subworkflowNode?.outputData?.actionHistory;
|
|
232
|
+
if (actionTrace.length === 0 && Array.isArray(subworkflowHistory) && subworkflowHistory.length > 0) {
|
|
233
|
+
subworkflowHistory.forEach((step, i) => {
|
|
234
|
+
actionTrace.push({
|
|
235
|
+
step: i + 1,
|
|
236
|
+
action: step.actionType ?? step.action_type ?? step.action,
|
|
237
|
+
intent: step.intent,
|
|
238
|
+
target: step.target,
|
|
239
|
+
value: step.value ?? undefined,
|
|
240
|
+
success: step.success ?? true,
|
|
241
|
+
durationMs: step.durationMs ?? step.duration_ms ?? undefined,
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// Evaluation: brain.evaluate (old) → subworkflow.run outcome/success (new)
|
|
219
246
|
const evalNode = nodes.find(n => n.nodeType === 'brain.evaluate');
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
247
|
+
let evaluation;
|
|
248
|
+
if (evalNode?.outputData) {
|
|
249
|
+
evaluation = {
|
|
250
|
+
passed: evalNode.outputData.passed,
|
|
251
|
+
outcome: evalNode.outputData.outcome,
|
|
252
|
+
reason: evalNode.outputData.reason,
|
|
253
|
+
verifications: evalNode.outputData.verifications,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
else if (subworkflowNode?.outputData) {
|
|
257
|
+
const sw = subworkflowNode.outputData;
|
|
258
|
+
evaluation = {
|
|
259
|
+
passed: sw.success,
|
|
260
|
+
outcome: sw.outcome,
|
|
261
|
+
reason: sw.error || undefined,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
228
264
|
const responsePayload = {
|
|
229
265
|
outcome,
|
|
230
|
-
success: finalExecution.state?.success ?? false,
|
|
266
|
+
success: finalExecution.state?.success ?? subworkflowNode?.outputData?.success ?? false,
|
|
231
267
|
status: finalExecution.status,
|
|
232
|
-
stepsTaken: finalExecution.state?.stepsTaken ?? actionTrace.length
|
|
268
|
+
stepsTaken: finalExecution.state?.stepsTaken ?? subworkflowNode?.outputData?.stepsTaken ?? actionTrace.length,
|
|
233
269
|
targetUrl: originalUrl,
|
|
234
270
|
executionId: executionUuid,
|
|
235
271
|
durationMs: finalExecution.durationMs ?? duration,
|
|
236
272
|
};
|
|
237
|
-
|
|
238
|
-
if (actionTrace.length > 0) {
|
|
273
|
+
if (actionTrace.length > 0)
|
|
239
274
|
responsePayload.actionTrace = actionTrace;
|
|
240
|
-
|
|
241
|
-
// The final evaluation — pass/fail with reasoning
|
|
242
|
-
if (evaluation) {
|
|
275
|
+
if (evaluation)
|
|
243
276
|
responsePayload.evaluation = evaluation;
|
|
244
|
-
}
|
|
245
277
|
if (finalExecution.state?.error)
|
|
246
278
|
responsePayload.agentError = finalExecution.state.error;
|
|
247
279
|
if (finalExecution.errorMessage)
|
|
@@ -262,15 +294,23 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
262
294
|
const content = [
|
|
263
295
|
{ type: 'text', text: JSON.stringify(responsePayload, null, 2) },
|
|
264
296
|
];
|
|
265
|
-
//
|
|
266
|
-
const
|
|
297
|
+
// Screenshot: check for already-base64 field first (subworkflow.run), then URL-based fields
|
|
298
|
+
const SCREENSHOT_URL_KEYS = ['finalScreenshot', 'screenshot', 'screenshotUrl', 'screenshotUri'];
|
|
267
299
|
const GIF_KEYS = ['runGif', 'gifUrl', 'gif', 'videoUrl', 'recordingUrl'];
|
|
268
|
-
let
|
|
300
|
+
let screenshotEmbedded = false;
|
|
269
301
|
let gifUrl = null;
|
|
270
|
-
|
|
302
|
+
// subworkflow.run carries screenshotB64 directly — no fetch needed
|
|
303
|
+
const screenshotB64 = subworkflowNode?.outputData?.screenshotB64;
|
|
304
|
+
if (typeof screenshotB64 === 'string' && screenshotB64) {
|
|
305
|
+
logger.info('Embedding inline base64 screenshot from subworkflow.run');
|
|
306
|
+
content.push(imageContentBlock(screenshotB64, 'image/png'));
|
|
307
|
+
screenshotEmbedded = true;
|
|
308
|
+
}
|
|
309
|
+
let screenshotUrl = null;
|
|
310
|
+
for (const node of nodes) {
|
|
271
311
|
const data = node.outputData ?? {};
|
|
272
|
-
if (!screenshotUrl) {
|
|
273
|
-
for (const key of
|
|
312
|
+
if (!screenshotEmbedded && !screenshotUrl) {
|
|
313
|
+
for (const key of SCREENSHOT_URL_KEYS) {
|
|
274
314
|
if (typeof data[key] === 'string' && data[key]) {
|
|
275
315
|
screenshotUrl = data[key];
|
|
276
316
|
break;
|
|
@@ -285,10 +325,10 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
285
325
|
}
|
|
286
326
|
}
|
|
287
327
|
}
|
|
288
|
-
if (screenshotUrl && gifUrl)
|
|
328
|
+
if ((screenshotEmbedded || screenshotUrl) && gifUrl)
|
|
289
329
|
break;
|
|
290
330
|
}
|
|
291
|
-
if (screenshotUrl) {
|
|
331
|
+
if (!screenshotEmbedded && screenshotUrl) {
|
|
292
332
|
logger.info(`Embedding screenshot: ${screenshotUrl}`);
|
|
293
333
|
const img = await fetchImageAsBase64(screenshotUrl).catch(() => null);
|
|
294
334
|
if (img)
|