testronaut 1.10.2 → 1.12.0
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/README.md +198 -0
- package/TODO.md +5 -0
- package/bin/cli.js +234 -5
- package/bin/terminalUi.js +51 -0
- package/core/agent.js +1 -0
- package/core/browserEvidence.js +184 -0
- package/core/jevRouting.js +77 -0
- package/core/jevShadow.js +325 -0
- package/core/jevSuiteSelection.js +39 -0
- package/core/jevUsage.js +62 -0
- package/core/optimization.js +98 -0
- package/core/turnLoop.js +174 -10
- package/experiments/CROSS_PROVIDER.md +19 -0
- package/experiments/README.md +62 -0
- package/experiments/buildExperimentPublicationPackage.js +219 -0
- package/experiments/buildOptimizationDataPackage.js +390 -0
- package/experiments/crossProviderBenchmark.js +227 -0
- package/experiments/optimizationBenchmark.js +339 -0
- package/package.json +5 -1
- package/tests/binTests/cli.helpers.test.js +16 -0
- package/tests/binTests/cli.integration.test.js +14 -0
- package/tests/binTests/terminalUi.test.js +34 -0
- package/tests/coreTests/browserEvidence.test.js +70 -0
- package/tests/coreTests/jevRouting.test.js +57 -0
- package/tests/coreTests/jevShadow.test.js +158 -0
- package/tests/coreTests/jevSuiteSelection.test.js +24 -0
- package/tests/coreTests/jevUsage.test.js +58 -0
- package/tests/coreTests/optimization.test.js +80 -0
- package/tests/coreTests/turnLoop.test.js +304 -0
- package/tests/experimentTests/buildOptimizationDataPackage.test.js +25 -0
- package/tests/experimentTests/crossProviderBenchmark.test.js +37 -0
- package/tests/experimentTests/optimizationBenchmark.test.js +49 -0
- package/tests/toolsTests/generateHtmlReport.test.js +83 -0
- package/tests/toolsTests/turnLoopUtils.test.js +17 -0
- package/tools/generateHtmlReport.js +96 -26
- package/tools/turnLoopUtils.js +7 -11
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export const OPTIMIZATION_OBJECTIVES = ['off', 'cost', 'tokens', 'balanced', 'speed'];
|
|
2
|
+
export const OPTIMIZATION_MODES = ['off', 'shadow', 'live'];
|
|
3
|
+
|
|
4
|
+
const OBJECTIVE_ALIASES = {
|
|
5
|
+
neither: 'off',
|
|
6
|
+
'lower-spend': 'cost',
|
|
7
|
+
spend: 'cost',
|
|
8
|
+
'fewer-tokens': 'tokens',
|
|
9
|
+
both: 'balanced',
|
|
10
|
+
latency: 'speed',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function normalize(value, allowed, aliases = {}) {
|
|
14
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
15
|
+
const resolved = aliases[normalized] || normalized;
|
|
16
|
+
return allowed.includes(resolved) ? resolved : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function truthy(value) {
|
|
20
|
+
return /^(1|true|yes|on)$/i.test(String(value ?? '').trim());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveOptimizationPolicy({ env = process.env, config = {} } = {}) {
|
|
24
|
+
const configValue = config.optimization || {};
|
|
25
|
+
const guardrailsValue = config.guardrails || {};
|
|
26
|
+
const guardrailsConfigured = guardrailsValue.enabled !== undefined || guardrailsValue.mode !== undefined;
|
|
27
|
+
const guardrailsEnabled = guardrailsValue.enabled !== false && guardrailsValue.mode !== 'off';
|
|
28
|
+
const rawObjective = env.TESTRONAUT_OPTIMIZE
|
|
29
|
+
?? configValue.objective
|
|
30
|
+
?? (guardrailsConfigured ? (guardrailsEnabled ? 'tokens' : 'off') : undefined);
|
|
31
|
+
const rawMode = env.TESTRONAUT_OPTIMIZATION_MODE
|
|
32
|
+
?? configValue.mode
|
|
33
|
+
?? (guardrailsConfigured ? (guardrailsEnabled ? (guardrailsValue.mode || 'live') : 'off') : undefined);
|
|
34
|
+
const objective = normalize(rawObjective, OPTIMIZATION_OBJECTIVES, OBJECTIVE_ALIASES);
|
|
35
|
+
const mode = normalize(rawMode, OPTIMIZATION_MODES);
|
|
36
|
+
const configured = rawObjective !== undefined || rawMode !== undefined;
|
|
37
|
+
const effectiveObjective = objective ?? (configured ? 'off' : null);
|
|
38
|
+
const effectiveMode = mode ?? (configured && effectiveObjective !== 'off' ? 'shadow' : 'off');
|
|
39
|
+
const routingRequested = ['cost', 'balanced', 'speed'].includes(effectiveObjective);
|
|
40
|
+
const betaModelRouting = truthy(
|
|
41
|
+
env.TESTRONAUT_BETA_MODEL_ROUTING ?? configValue.beta?.modelRouting,
|
|
42
|
+
);
|
|
43
|
+
const threshold = Number(
|
|
44
|
+
env.TESTRONAUT_JEV_ROUTING_THRESHOLD
|
|
45
|
+
?? configValue.guardrails?.minimumRoutingProbability,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
configured,
|
|
50
|
+
objective: effectiveObjective,
|
|
51
|
+
mode: effectiveObjective === 'off' ? 'off' : effectiveMode,
|
|
52
|
+
routingRequested,
|
|
53
|
+
betaModelRouting,
|
|
54
|
+
routing: routingRequested && betaModelRouting,
|
|
55
|
+
tokenReduction: ['tokens', 'balanced'].includes(effectiveObjective),
|
|
56
|
+
completionGate: effectiveMode === 'live' && effectiveObjective !== 'off',
|
|
57
|
+
routingProbability: Number.isFinite(threshold) && threshold >= 0 && threshold <= 1
|
|
58
|
+
? threshold
|
|
59
|
+
: 0.4,
|
|
60
|
+
completionProbability: (() => {
|
|
61
|
+
const value = Number(
|
|
62
|
+
env.TESTRONAUT_JEV_GATE_THRESHOLD
|
|
63
|
+
?? guardrailsValue.completionProbability
|
|
64
|
+
?? configValue.guardrails?.completionProbability,
|
|
65
|
+
);
|
|
66
|
+
return Number.isFinite(value) && value >= 0 && value <= 1 ? value : 0.8;
|
|
67
|
+
})(),
|
|
68
|
+
valid: (!configured || Boolean(objective || rawObjective === undefined))
|
|
69
|
+
&& (rawMode === undefined || Boolean(mode)),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function applyOptimizationPolicy(policy, env = process.env, { force = false } = {}) {
|
|
74
|
+
if (!policy?.configured) return env;
|
|
75
|
+
env.TESTRONAUT_OPTIMIZATION_OBJECTIVE_EFFECTIVE = policy.objective;
|
|
76
|
+
env.TESTRONAUT_OPTIMIZATION_MODE_EFFECTIVE = policy.mode;
|
|
77
|
+
if (force || env.TESTRONAUT_JEV_ROUTING_THRESHOLD === undefined) {
|
|
78
|
+
env.TESTRONAUT_JEV_ROUTING_THRESHOLD = String(policy.routingProbability);
|
|
79
|
+
}
|
|
80
|
+
if (force || env.TESTRONAUT_JEV_GATE_THRESHOLD === undefined) {
|
|
81
|
+
env.TESTRONAUT_JEV_GATE_THRESHOLD = String(policy.completionProbability);
|
|
82
|
+
}
|
|
83
|
+
if (force || env.TESTRONAUT_JEV_ROUTING === undefined) {
|
|
84
|
+
env.TESTRONAUT_JEV_ROUTING = policy.routing ? policy.mode : 'off';
|
|
85
|
+
}
|
|
86
|
+
if (force || env.TESTRONAUT_JEV_GATE === undefined) {
|
|
87
|
+
env.TESTRONAUT_JEV_GATE = policy.completionGate ? '1' : '0';
|
|
88
|
+
}
|
|
89
|
+
if (force || env.TESTRONAUT_JEV_STRATEGIES === undefined) {
|
|
90
|
+
env.TESTRONAUT_JEV_STRATEGIES = policy.routing ? '1' : '0';
|
|
91
|
+
}
|
|
92
|
+
if (force || env.TESTRONAUT_JEV_SHADOW === undefined) {
|
|
93
|
+
env.TESTRONAUT_JEV_SHADOW = policy.mode === 'off' ? '0' : '1';
|
|
94
|
+
}
|
|
95
|
+
return env;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const __optimizationInternals = { normalize, OBJECTIVE_ALIASES };
|
package/core/turnLoop.js
CHANGED
|
@@ -49,7 +49,10 @@ import {
|
|
|
49
49
|
import { resolveProviderModel } from '../llm/modelResolver.js';
|
|
50
50
|
import { getLLM } from '../llm/llmFactory.js';
|
|
51
51
|
import { summarizeTurnIntentFromMessage } from './turnIntent.js';
|
|
52
|
-
import { maskPreview, redactArgs } from './redaction.js';
|
|
52
|
+
import { maskPreview, redactArgs, redactPasswordInText } from './redaction.js';
|
|
53
|
+
import { buildJevShadowState, buildJevStrategyQuestions, evaluateJevCompletionCandidate, evaluateJevShadow, extractJevStrategyResults, resolveJevShadowConfig, sanitizeJevShadowEvents } from './jevShadow.js';
|
|
54
|
+
import { buildBrowserEvidence, collectBrowserControlState, mergeBrowserControlState } from './browserEvidence.js';
|
|
55
|
+
import { chooseJevTurnModel, resolveJevRoutingConfig } from './jevRouting.js';
|
|
53
56
|
import {
|
|
54
57
|
sanitizeHeavyToolHistory,
|
|
55
58
|
pruneConversationContext,
|
|
@@ -169,6 +172,20 @@ const FIRE_AND_FORGET_TOOLS = new Set([
|
|
|
169
172
|
'record_mission_telemetry',
|
|
170
173
|
]);
|
|
171
174
|
|
|
175
|
+
function currentBrowserUrl(browser) {
|
|
176
|
+
try { return browser?.page?.url?.() || null; } catch { return null; }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function captureBrowserEvidence(browser, html, previous) {
|
|
180
|
+
const evidence = buildBrowserEvidence(html, {
|
|
181
|
+
url: currentBrowserUrl(browser),
|
|
182
|
+
previous,
|
|
183
|
+
redactText: redactPasswordInText,
|
|
184
|
+
});
|
|
185
|
+
const states = await collectBrowserControlState(browser);
|
|
186
|
+
return mergeBrowserControlState(evidence, states);
|
|
187
|
+
}
|
|
188
|
+
|
|
172
189
|
/**
|
|
173
190
|
* Utility: format byte counts into human-readable strings.
|
|
174
191
|
* Used when reporting upload/download events in mission logs.
|
|
@@ -221,6 +238,11 @@ const pushDOMAssistant = async (browser, messages, agentMemory, { skipIfLastTool
|
|
|
221
238
|
agentMemory
|
|
222
239
|
);
|
|
223
240
|
await tokenEstimate(MODEL_ID, domHtml);
|
|
241
|
+
agentMemory.browserEvidence = await captureBrowserEvidence(
|
|
242
|
+
browser,
|
|
243
|
+
domHtml,
|
|
244
|
+
agentMemory.browserEvidence,
|
|
245
|
+
);
|
|
224
246
|
|
|
225
247
|
// Push the corresponding tool response back into the conversation
|
|
226
248
|
messages.push({
|
|
@@ -269,6 +291,11 @@ export const turnLoop = async (
|
|
|
269
291
|
dataTypes: ['document', 'file', 'item', 'row'],
|
|
270
292
|
};
|
|
271
293
|
const stepsArchive = ctx.stepsArchive || steps;
|
|
294
|
+
const jevShadowConfig = resolveJevShadowConfig();
|
|
295
|
+
const jevRoutingConfig = resolveJevRoutingConfig(process.env, {
|
|
296
|
+
provider: PROVIDER_ID,
|
|
297
|
+
primaryModel: MODEL_ID,
|
|
298
|
+
});
|
|
272
299
|
const retryLimitClamped = Math.min(10, Math.max(1, Number.isFinite(retryLimitRaw) ? retryLimitRaw : DEFAULT_TURN_RETRY_LIMIT)); // retries allowed (excludes initial)
|
|
273
300
|
const maxAttempts = retryLimitClamped + 1; // includes initial attempt
|
|
274
301
|
ctx.groundControl = groundControl;
|
|
@@ -281,6 +308,102 @@ export const turnLoop = async (
|
|
|
281
308
|
ensureDocProgress(agentMemory, resourceGuardCfg);
|
|
282
309
|
let turnRetries = 0;
|
|
283
310
|
let stepSeq = ctx._stepSeq || 0;
|
|
311
|
+
let nextJevStrategy = null;
|
|
312
|
+
let forcePrimaryRetry = false;
|
|
313
|
+
|
|
314
|
+
const recordJevShadow = async (step, msg, phase, proposedTools = []) => {
|
|
315
|
+
if (!jevShadowConfig.enabled) return;
|
|
316
|
+
nextJevStrategy = null;
|
|
317
|
+
// Keep status lines and purpose-built telemetry, but exclude raw tool payload
|
|
318
|
+
// lines (which may contain DOM, page text, or other third-party data).
|
|
319
|
+
const safeEvents = sanitizeJevShadowEvents(step.events, redactPasswordInText);
|
|
320
|
+
agentMemory.browserEvidence = await captureBrowserEvidence(
|
|
321
|
+
browser,
|
|
322
|
+
null,
|
|
323
|
+
agentMemory.browserEvidence,
|
|
324
|
+
);
|
|
325
|
+
const state = buildJevShadowState({
|
|
326
|
+
mission: redactPasswordInText(ctx.goal || missionName),
|
|
327
|
+
turn: step.turn,
|
|
328
|
+
maxTurns,
|
|
329
|
+
phase,
|
|
330
|
+
assistantSummary: redactPasswordInText(step.summary || ''),
|
|
331
|
+
assistantContent: redactPasswordInText(msg?.content || ''),
|
|
332
|
+
proposedTools,
|
|
333
|
+
events: safeEvents,
|
|
334
|
+
groundControl,
|
|
335
|
+
browserEvidence: agentMemory.browserEvidence,
|
|
336
|
+
});
|
|
337
|
+
const strategyCatalog = jevShadowConfig.strategiesEnabled
|
|
338
|
+
? buildJevStrategyQuestions(agentMemory.browserEvidence)
|
|
339
|
+
: null;
|
|
340
|
+
step.jevShadow = await evaluateJevShadow(state, {
|
|
341
|
+
config: jevShadowConfig,
|
|
342
|
+
...(strategyCatalog ? { questions: strategyCatalog.questions } : {}),
|
|
343
|
+
});
|
|
344
|
+
if (step.jevShadow.status === 'ok' && strategyCatalog) {
|
|
345
|
+
step.jevShadow.strategies = extractJevStrategyResults(
|
|
346
|
+
step.jevShadow.answers,
|
|
347
|
+
strategyCatalog,
|
|
348
|
+
);
|
|
349
|
+
nextJevStrategy = step.jevShadow.strategies;
|
|
350
|
+
}
|
|
351
|
+
step.jevShadow.completionGate = evaluateJevCompletionCandidate(step.jevShadow, {
|
|
352
|
+
threshold: jevShadowConfig.gateThreshold,
|
|
353
|
+
browserEvidence: agentMemory.browserEvidence,
|
|
354
|
+
});
|
|
355
|
+
const status = step.jevShadow?.status || 'unknown';
|
|
356
|
+
const latency = Number.isFinite(step.jevShadow?.latencyMs)
|
|
357
|
+
? ` in ${step.jevShadow.latencyMs}ms`
|
|
358
|
+
: '';
|
|
359
|
+
console.log(`🔬 Jev shadow: ${status}${latency}`);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const finalizeWithJevGate = async (step) => {
|
|
363
|
+
if (!jevShadowConfig.gateEnabled || !step.jevShadow?.completionGate?.candidate) return null;
|
|
364
|
+
|
|
365
|
+
const prog = ensureDocProgress(agentMemory, resourceGuardCfg);
|
|
366
|
+
if (prog?.items.length && prog.downloaded.size < prog.items.length) {
|
|
367
|
+
step.jevGate = { triggered: false, reason: 'resource_guard_incomplete' };
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const existingScreenshot = Boolean(
|
|
372
|
+
step.screenshotPath
|
|
373
|
+
|| steps.some(existing => existing?.screenshotPath)
|
|
374
|
+
|| stepsArchive.some(existing => existing?.screenshotPath),
|
|
375
|
+
);
|
|
376
|
+
if (process.env.TESTRONAUT_SCREENSHOTS !== '0' && !existingScreenshot) {
|
|
377
|
+
try {
|
|
378
|
+
const screenshotResult = await CHROME_TOOL_MAP.screenshot(
|
|
379
|
+
browser,
|
|
380
|
+
{ name: `jev-completion-turn-${step.turn + 1}` },
|
|
381
|
+
agentMemory,
|
|
382
|
+
);
|
|
383
|
+
const match = String(screenshotResult).match(/screenshot.*?saved at: (.+\.png)/i);
|
|
384
|
+
if (match?.[1]) step.screenshotPath = match[1];
|
|
385
|
+
step.events.push('🖼️ Jev completion gate captured final screenshot');
|
|
386
|
+
} catch (error) {
|
|
387
|
+
step.jevGate = { triggered: false, reason: 'screenshot_failed', error: error?.message || String(error) };
|
|
388
|
+
step.events.push(`⚠️ Jev completion gate screenshot failed: ${error?.message || error}`);
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const gate = step.jevShadow.completionGate;
|
|
394
|
+
recordGroundTelemetry(groundControl, {
|
|
395
|
+
kind: 'assertion',
|
|
396
|
+
text: `Jev completion gate satisfied at ${Math.round(gate.threshold * 100)}% threshold.`,
|
|
397
|
+
status: 'passed',
|
|
398
|
+
}, { turn: step.turn, source: 'jev_completion_gate', signals: gate.signals });
|
|
399
|
+
|
|
400
|
+
step.jevGate = { triggered: true, ...gate };
|
|
401
|
+
step.events.push(`🔬 Jev completion gate triggered at ${Math.round(gate.threshold * 100)}% threshold`);
|
|
402
|
+
step.result = '✅ Mission Success (Jev gate)';
|
|
403
|
+
recordStep(step);
|
|
404
|
+
const finalMessage = 'SUCCESS: Jev completion gate found sufficient browser evidence.';
|
|
405
|
+
return { success: true, finalMessage, steps: stepsArchive, completedBy: 'jev_gate' };
|
|
406
|
+
};
|
|
284
407
|
|
|
285
408
|
// Centralized recorder: push to in-memory buffer AND fire optional callback for streaming
|
|
286
409
|
// Idempotent recorder – prevents accidental double-push
|
|
@@ -310,6 +433,20 @@ export const turnLoop = async (
|
|
|
310
433
|
result: '🟡 In Progress',
|
|
311
434
|
missionName,
|
|
312
435
|
};
|
|
436
|
+
const modelRouting = chooseJevTurnModel(nextJevStrategy, jevRoutingConfig, {
|
|
437
|
+
provider: PROVIDER_ID,
|
|
438
|
+
primaryModel: MODEL_ID,
|
|
439
|
+
});
|
|
440
|
+
if (forcePrimaryRetry) {
|
|
441
|
+
modelRouting.selectedModel = MODEL_ID;
|
|
442
|
+
modelRouting.applied = false;
|
|
443
|
+
modelRouting.reason = 'routed_retry_primary';
|
|
444
|
+
modelRouting.forcedPrimaryRetry = true;
|
|
445
|
+
forcePrimaryRetry = false;
|
|
446
|
+
}
|
|
447
|
+
step.modelRouting = modelRouting;
|
|
448
|
+
step.provider = PROVIDER_ID;
|
|
449
|
+
step.model = modelRouting.selectedModel;
|
|
313
450
|
if (attempt > 1) {
|
|
314
451
|
const retryNumber = attempt - 1;
|
|
315
452
|
step.events.push(`🔁 Re-attempt ${retryNumber}/${retryLimitClamped} for turn`);
|
|
@@ -372,13 +509,14 @@ export const turnLoop = async (
|
|
|
372
509
|
// ─────────────────────────────────────────────
|
|
373
510
|
// STEP 2: Request next reasoning turn from model
|
|
374
511
|
// ─────────────────────────────────────────────
|
|
375
|
-
const
|
|
512
|
+
const turnModel = modelRouting.selectedModel;
|
|
513
|
+
const contextCheck = await warnIfContextNearLimit(turnModel, { messages, tools: activeToolsSchema });
|
|
376
514
|
const projectedTokens = contextCheck.estimatedTokens
|
|
377
|
-
?? await tokenEstimate(
|
|
515
|
+
?? await tokenEstimate(turnModel, { messages, tools: activeToolsSchema });
|
|
378
516
|
({ totalTokensUsed, turnTimestamps, shouldBackoff } = await tokenUseCoolOff(
|
|
379
517
|
totalTokensUsed,
|
|
380
518
|
turnTimestamps,
|
|
381
|
-
|
|
519
|
+
turnModel,
|
|
382
520
|
projectedTokens
|
|
383
521
|
));
|
|
384
522
|
if (shouldBackoff) {
|
|
@@ -386,13 +524,21 @@ export const turnLoop = async (
|
|
|
386
524
|
turn -= 1;
|
|
387
525
|
continue;
|
|
388
526
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
messages,
|
|
392
|
-
|
|
393
|
-
|
|
527
|
+
let modelResponse;
|
|
528
|
+
try {
|
|
529
|
+
modelResponse = await llm.chat({ model: turnModel, messages, tools: activeToolsSchema });
|
|
530
|
+
} catch (routingError) {
|
|
531
|
+
if (!modelRouting.applied) throw routingError;
|
|
532
|
+
modelRouting.fallback = true;
|
|
533
|
+
modelRouting.fallbackReason = routingError?.message || String(routingError);
|
|
534
|
+
modelRouting.selectedModel = MODEL_ID;
|
|
535
|
+
step.model = MODEL_ID;
|
|
536
|
+
step.events.push(`↩️ Jev route fallback to ${MODEL_ID}`);
|
|
537
|
+
modelResponse = await llm.chat({ model: MODEL_ID, messages, tools: activeToolsSchema });
|
|
538
|
+
}
|
|
539
|
+
const { message, usage, headers } = modelResponse;
|
|
394
540
|
response = { message, usage, headers };
|
|
395
|
-
updateLimitsFromHeaders(
|
|
541
|
+
updateLimitsFromHeaders(modelRouting.selectedModel, headers);
|
|
396
542
|
|
|
397
543
|
} catch (err) {
|
|
398
544
|
// ─────────────────────────────────────────────
|
|
@@ -460,6 +606,7 @@ export const turnLoop = async (
|
|
|
460
606
|
if (msg.tool_calls?.length) {
|
|
461
607
|
console.log('Processing tool calls...');
|
|
462
608
|
const toolResponses = [];
|
|
609
|
+
const proposedTools = [];
|
|
463
610
|
let hadToolIssues = false;
|
|
464
611
|
let domRefreshSource = null;
|
|
465
612
|
|
|
@@ -467,6 +614,7 @@ export const turnLoop = async (
|
|
|
467
614
|
const fnName = call.function.name;
|
|
468
615
|
const args = JSON.parse(call.function.arguments || '{}');
|
|
469
616
|
const safeArgs = redactArgs(fnName, args);
|
|
617
|
+
proposedTools.push({ name: fnName, arguments: safeArgs });
|
|
470
618
|
console.log(`[model] → ${fnName}`, safeArgs);
|
|
471
619
|
step.events.push(`[model] → ${fnName} ${JSON.stringify(safeArgs)}`);
|
|
472
620
|
|
|
@@ -761,6 +909,14 @@ export const turnLoop = async (
|
|
|
761
909
|
}
|
|
762
910
|
}
|
|
763
911
|
|
|
912
|
+
if (!errorMessage && fnName === 'get_dom') {
|
|
913
|
+
agentMemory.browserEvidence = await captureBrowserEvidence(
|
|
914
|
+
browser,
|
|
915
|
+
result,
|
|
916
|
+
agentMemory.browserEvidence,
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
764
920
|
// Decide what to send back to the LLM for this tool.
|
|
765
921
|
// - For "fire-and-forget" tools, send a tiny stub (OK/error) to avoid
|
|
766
922
|
// bloating context with large payloads (file JSON, etc.).
|
|
@@ -833,11 +989,17 @@ export const turnLoop = async (
|
|
|
833
989
|
step.events.push(`[auto] → DOM size after ${domRefreshSource}: ${domRefresh.domLength} chars`);
|
|
834
990
|
}
|
|
835
991
|
}
|
|
992
|
+
await recordJevShadow(step, msg, hadToolIssues ? 'after_action_error' : 'after_action', proposedTools);
|
|
993
|
+
if (!hadToolIssues) {
|
|
994
|
+
const gatedResult = await finalizeWithJevGate(step);
|
|
995
|
+
if (gatedResult) return gatedResult;
|
|
996
|
+
}
|
|
836
997
|
if (hadToolIssues && turnRetries < retryLimitClamped) {
|
|
837
998
|
step.result = '⏳ Retrying turn';
|
|
838
999
|
const retryNumber = attempt - 1;
|
|
839
1000
|
step.events.push(`🔁 Re-attempt ${retryNumber}/${retryLimitClamped} after tool issues`);
|
|
840
1001
|
recordStep(step);
|
|
1002
|
+
forcePrimaryRetry = modelRouting.applied;
|
|
841
1003
|
turnRetries += 1;
|
|
842
1004
|
const delay = Math.min(TURN_RETRY_BASE_DELAY_MS * 2 ** (turnRetries - 1), 2000);
|
|
843
1005
|
await wait(delay);
|
|
@@ -875,6 +1037,7 @@ export const turnLoop = async (
|
|
|
875
1037
|
turnRetries = 0;
|
|
876
1038
|
continue;
|
|
877
1039
|
}
|
|
1040
|
+
await recordJevShadow(step, msg, 'final_response');
|
|
878
1041
|
step.events.push(finalResponse.finalMessage);
|
|
879
1042
|
step.result = finalResponse.success ? '✅ Mission Success' : '❌ Mission Failure';
|
|
880
1043
|
recordStep(step);
|
|
@@ -891,6 +1054,7 @@ export const turnLoop = async (
|
|
|
891
1054
|
});
|
|
892
1055
|
console.log(`[auto] → Injected DOM for next reasoning step`);
|
|
893
1056
|
step.events.push(`[auto] → Injected DOM for next reasoning step`);
|
|
1057
|
+
await recordJevShadow(step, msg, 'after_observation');
|
|
894
1058
|
// This is a meaningful turn even without tool calls — record it.
|
|
895
1059
|
step.result = step.result || '🟡 In Progress';
|
|
896
1060
|
recordStep(step);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Cross-provider benchmark
|
|
2
|
+
|
|
3
|
+
This benchmark runs three example-repository missions across four fixed-model controls and four two-model OpenAI/Gemini portfolios. Jev selects one model at the mission boundary for portfolio conditions; that model owns the complete mission transcript.
|
|
4
|
+
|
|
5
|
+
The default design is 8 conditions × 5 randomized blocks × 3 missions, or 120 isolated mission executions. Failed missions are not retried with the other provider.
|
|
6
|
+
|
|
7
|
+
Preview the complete deterministic schedule without making API calls:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm run benchmark:cross-provider -- --dry-run
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run or resume it:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm run benchmark:cross-provider
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Keys are loaded from `/home/shane/workspace/testronaut-examples/.env`. Progress, selections, logs, report paths, and a summary CSV are written under `experiments/results/cross-provider-pilot-v1/`. Re-running the command resumes completed work. Useful overrides include `--repetitions=5`, `--seed=...`, `--repo=...`, `--experiment-id=...`, and `--selection-threshold=0.55`.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Optimization benchmark pilot
|
|
2
|
+
|
|
3
|
+
This benchmark compares seven optimization conditions across the OpenAI-focused
|
|
4
|
+
Testronaut Examples repository and the Gemini-focused Collegium repository. It
|
|
5
|
+
uses five randomized blocks by default: 35 runs per repository and 70 total.
|
|
6
|
+
|
|
7
|
+
The fixed mission subsets are:
|
|
8
|
+
|
|
9
|
+
- OpenAI: login, self-cleaning task creation/removal, and file transfer.
|
|
10
|
+
- Gemini: profile navigation, projects dashboard, and project workspace navigation.
|
|
11
|
+
|
|
12
|
+
Preview the complete deterministic schedule without making model calls:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm run benchmark:optimization -- --dry-run
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Run or resume both repositories:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm run benchmark:optimization
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Run only one target:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm run benchmark:optimization -- --target=openai
|
|
28
|
+
npm run benchmark:optimization -- --target=gemini
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Useful controls:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm run benchmark:optimization -- --repetitions=1 --experiment-id=optimization-smoke
|
|
35
|
+
npm run benchmark:optimization -- --seed=my-fixed-seed
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Progress is saved after every condition under target-specific
|
|
39
|
+
`experiments/results/<experiment-id>/state-openai.json` and
|
|
40
|
+
`state-gemini.json` files, so both loops may run concurrently without clobbering
|
|
41
|
+
state. Reusing the same experiment ID skips completed conditions. The matching
|
|
42
|
+
`runs-openai.csv` and `runs-gemini.csv` files contain one row per completed run,
|
|
43
|
+
and captured CLI output is stored under the corresponding `logs` directory.
|
|
44
|
+
Each generated Testronaut JSON report also records its experiment ID, condition,
|
|
45
|
+
and block. If state is lost, the runner recovers completed conditions from those
|
|
46
|
+
report fields before resuming.
|
|
47
|
+
|
|
48
|
+
The runner invokes this checkout's `bin/cli.js` directly and fixes the routing
|
|
49
|
+
probability at 0.40. It does not alter either target repository's configuration.
|
|
50
|
+
Model/API credentials must already be available in the environment or target
|
|
51
|
+
repository `.env` files.
|
|
52
|
+
|
|
53
|
+
After all 70 reports are present, generate the findings report and portable data
|
|
54
|
+
package with:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm run benchmark:package
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This writes a Markdown report, normalized CSV datasets, the raw JSON reports,
|
|
61
|
+
manifest, and checksums under `experiments/results/optimization-pilot-v1/`, plus
|
|
62
|
+
a compressed `optimization-pilot-v1-data-package.tar.gz` archive.
|