@iamu-dev/iamu 0.2.0-beta.1 → 0.2.0-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/dist/bin.js +2178 -330
- package/dist/commands/browse.d.ts +14 -0
- package/dist/commands/complete.d.ts +1 -0
- package/dist/commands/credentials.d.ts +1 -1
- package/dist/commands/doctor.d.ts +11 -2
- package/dist/commands/evaluate.d.ts +2 -0
- package/dist/commands/explore-crawl.d.ts +67 -0
- package/dist/commands/explore.d.ts +52 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/instructions-walk.d.ts +11 -1
- package/dist/commands/match.d.ts +1 -0
- package/dist/commands/new-entity.d.ts +13 -0
- package/dist/commands/start-run.d.ts +7 -2
- package/dist/eval/gemini-eval-cli.d.ts +10 -0
- package/dist/eval/gemini-eval-cli.js +976 -0
- package/dist/eval/gemini-walker.d.ts +46 -0
- package/dist/eval/walkthrough-evaluator.d.ts +61 -0
- package/dist/index.js +2174 -326
- package/dist/templates/prompts/walk.md +16 -4
- package/dist/templates/skills/iamu-findings.md +70 -32
- package/dist/templates/skills/iamu-new.md +123 -66
- package/dist/templates/skills/iamu-propose.md +60 -27
- package/dist/templates/skills/iamu-run.md +180 -60
- package/dist/templates/skills/iamu-setup.md +195 -117
- package/dist/templates/skills/iamu-status.md +21 -14
- package/package.json +6 -3
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/eval/walkthrough-evaluator.ts
|
|
12
|
+
var walkthrough_evaluator_exports = {};
|
|
13
|
+
__export(walkthrough_evaluator_exports, {
|
|
14
|
+
evaluateWalkthroughRun: () => evaluateWalkthroughRun,
|
|
15
|
+
formatEvaluationScorecardTable: () => formatEvaluationScorecardTable
|
|
16
|
+
});
|
|
17
|
+
function evaluateWalkthroughRun(canonical, contract) {
|
|
18
|
+
const findings = canonical.findings ?? [];
|
|
19
|
+
const steps = canonical.steps ?? [];
|
|
20
|
+
const unseededKeywords = contract.unseededDataKeywords ?? DEFAULT_UNSEEDED_KEYWORDS;
|
|
21
|
+
const expectedDefects = contract.expectedDefects ?? [];
|
|
22
|
+
let matchedDefects = 0;
|
|
23
|
+
for (const expected of expectedDefects) {
|
|
24
|
+
const isMatched = findings.some((f) => {
|
|
25
|
+
const text = `${f.title} ${f.ui_fact ?? ""} ${f.consequence ?? ""}`.toLowerCase();
|
|
26
|
+
return expected.keywords.some((kw) => text.includes(kw.toLowerCase()));
|
|
27
|
+
});
|
|
28
|
+
if (isMatched) {
|
|
29
|
+
matchedDefects++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const defectRecallRate = expectedDefects.length > 0 ? matchedDefects / expectedDefects.length : 1;
|
|
33
|
+
let environmentalFalsePositives = 0;
|
|
34
|
+
for (const f of findings) {
|
|
35
|
+
const text = `${f.title} ${f.ui_fact ?? ""} ${f.consequence ?? ""}`.toLowerCase();
|
|
36
|
+
const isEnvBug = unseededKeywords.some(
|
|
37
|
+
(kw) => text.includes(kw.toLowerCase())
|
|
38
|
+
);
|
|
39
|
+
if (isEnvBug && contract.expectedState === "empty") {
|
|
40
|
+
environmentalFalsePositives++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const fpreRate = findings.length > 0 ? environmentalFalsePositives / findings.length : 0;
|
|
44
|
+
const legitimateDefects = findings.length - environmentalFalsePositives;
|
|
45
|
+
const precisionRate = findings.length > 0 ? legitimateDefects / findings.length : 1;
|
|
46
|
+
let adhered = true;
|
|
47
|
+
let parReason = "Preconditions respected.";
|
|
48
|
+
if (contract.expectedState === "empty") {
|
|
49
|
+
if (environmentalFalsePositives > 0) {
|
|
50
|
+
adhered = false;
|
|
51
|
+
parReason = `Walker logged ${environmentalFalsePositives} defect(s) on unseeded empty state instead of honoring precondition.`;
|
|
52
|
+
}
|
|
53
|
+
} else if (contract.expectedState === "mismatch") {
|
|
54
|
+
const target = contract.expectedApproximationTarget?.toLowerCase();
|
|
55
|
+
if (target) {
|
|
56
|
+
const selectedTarget = steps.some((s) => {
|
|
57
|
+
const reasoning = s.persona_reasoning ?? s.note ?? "";
|
|
58
|
+
const text = `${s.action ?? ""} ${reasoning}`.toLowerCase();
|
|
59
|
+
return text.includes(target);
|
|
60
|
+
});
|
|
61
|
+
if (!selectedTarget) {
|
|
62
|
+
adhered = false;
|
|
63
|
+
parReason = `Walker failed to select approximation target "${contract.expectedApproximationTarget}".`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const allowedSurfaces = contract.allowedSurfaces;
|
|
68
|
+
let driftCount = 0;
|
|
69
|
+
if (allowedSurfaces && allowedSurfaces.length > 0) {
|
|
70
|
+
for (const s of steps) {
|
|
71
|
+
if (s.surface && s.surface.toLowerCase() !== "page" && !allowedSurfaces.some(
|
|
72
|
+
(allowed) => s.surface?.toLowerCase().includes(allowed.toLowerCase())
|
|
73
|
+
)) {
|
|
74
|
+
driftCount++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const totalSteps = steps.length;
|
|
79
|
+
const optimalSteps = contract.optimalSteps;
|
|
80
|
+
const stepWasteIndex = optimalSteps > 0 ? Math.max(0, (totalSteps - optimalSteps) / optimalSteps) : 0;
|
|
81
|
+
let authDetected = false;
|
|
82
|
+
let authTransitionSteps = 0;
|
|
83
|
+
let authSuccess = true;
|
|
84
|
+
if (contract.authRequired) {
|
|
85
|
+
const loginStepIndex = steps.findIndex((s) => {
|
|
86
|
+
const reasoning = s.persona_reasoning ?? s.note ?? "";
|
|
87
|
+
const text = `${s.action ?? ""} ${reasoning} ${s.url ?? ""}`.toLowerCase();
|
|
88
|
+
return text.includes("log") || text.includes("auth") || text.includes("inlogg");
|
|
89
|
+
});
|
|
90
|
+
if (loginStepIndex >= 0) {
|
|
91
|
+
authDetected = true;
|
|
92
|
+
authTransitionSteps = loginStepIndex + 1;
|
|
93
|
+
authSuccess = steps.length > authTransitionSteps;
|
|
94
|
+
} else {
|
|
95
|
+
authSuccess = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const passed = defectRecallRate >= 0.8 && precisionRate >= 0.8 && environmentalFalsePositives === 0 && adhered && (!contract.authRequired || authSuccess) && driftCount === 0;
|
|
99
|
+
const summary = passed ? `PASSED: Walkthrough satisfied all ground-truth contracts (Recall: ${(defectRecallRate * 100).toFixed(0)}%, Prec: ${(precisionRate * 100).toFixed(0)}%, FPR-E: 0%)` : `FAILED: Walkthrough violated contracts (FPR-E: ${(fpreRate * 100).toFixed(0)}%, PAR: ${adhered ? "Pass" : parReason}, Drift: ${driftCount})`;
|
|
100
|
+
return {
|
|
101
|
+
scenarioSlug: contract.scenarioSlug,
|
|
102
|
+
runId: canonical.run_id ?? "unknown",
|
|
103
|
+
expectedState: contract.expectedState,
|
|
104
|
+
defectRecall: {
|
|
105
|
+
matched: matchedDefects,
|
|
106
|
+
expected: expectedDefects.length,
|
|
107
|
+
rate: defectRecallRate
|
|
108
|
+
},
|
|
109
|
+
defectPrecision: {
|
|
110
|
+
legitimate: legitimateDefects,
|
|
111
|
+
total: findings.length,
|
|
112
|
+
rate: precisionRate
|
|
113
|
+
},
|
|
114
|
+
falsePositiveRateEnvironment: {
|
|
115
|
+
count: environmentalFalsePositives,
|
|
116
|
+
totalFindings: findings.length,
|
|
117
|
+
rate: fpreRate
|
|
118
|
+
},
|
|
119
|
+
preconditionAdherence: {
|
|
120
|
+
adhered,
|
|
121
|
+
reason: parReason
|
|
122
|
+
},
|
|
123
|
+
trajectory: {
|
|
124
|
+
totalSteps,
|
|
125
|
+
optimalSteps,
|
|
126
|
+
driftCount,
|
|
127
|
+
stepWasteIndex
|
|
128
|
+
},
|
|
129
|
+
authTransition: {
|
|
130
|
+
detected: authDetected,
|
|
131
|
+
transitionSteps: authTransitionSteps,
|
|
132
|
+
success: authSuccess
|
|
133
|
+
},
|
|
134
|
+
passed,
|
|
135
|
+
summary
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function formatEvaluationScorecardTable(scorecards) {
|
|
139
|
+
const lines = [];
|
|
140
|
+
lines.push(
|
|
141
|
+
"\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"
|
|
142
|
+
);
|
|
143
|
+
lines.push(
|
|
144
|
+
"\u2502 Benchmark Scenario \u2502 State \u2502 Recall \u2502 Prec. \u2502 FPR-E \u2502 PAR \u2502 Verdict\u2502"
|
|
145
|
+
);
|
|
146
|
+
lines.push(
|
|
147
|
+
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"
|
|
148
|
+
);
|
|
149
|
+
for (const s of scorecards) {
|
|
150
|
+
const name = s.scenarioSlug.padEnd(33).slice(0, 33);
|
|
151
|
+
const state = s.expectedState.padEnd(7).slice(0, 7);
|
|
152
|
+
const recallStr = s.defectRecall.expected > 0 ? `${s.defectRecall.matched}/${s.defectRecall.expected} (${(s.defectRecall.rate * 100).toFixed(0)}%)`.padEnd(
|
|
153
|
+
8
|
|
154
|
+
) : "N/A ";
|
|
155
|
+
const precStr = s.defectPrecision.total > 0 ? `${(s.defectPrecision.rate * 100).toFixed(0)}%`.padEnd(6) : "N/A ";
|
|
156
|
+
const fpreStr = `${(s.falsePositiveRateEnvironment.rate * 100).toFixed(0)}%`.padEnd(6);
|
|
157
|
+
const parStr = (s.preconditionAdherence.adhered ? "100%" : "0%").padEnd(5);
|
|
158
|
+
const verdict = s.passed ? "PASS " : "FAIL ";
|
|
159
|
+
lines.push(
|
|
160
|
+
`\u2502 ${name} \u2502 ${state} \u2502 ${recallStr} \u2502 ${precStr} \u2502 ${fpreStr} \u2502 ${parStr} \u2502 ${verdict}\u2502`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
lines.push(
|
|
164
|
+
"\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"
|
|
165
|
+
);
|
|
166
|
+
return lines.join("\n");
|
|
167
|
+
}
|
|
168
|
+
var DEFAULT_UNSEEDED_KEYWORDS;
|
|
169
|
+
var init_walkthrough_evaluator = __esm({
|
|
170
|
+
"src/eval/walkthrough-evaluator.ts"() {
|
|
171
|
+
"use strict";
|
|
172
|
+
DEFAULT_UNSEEDED_KEYWORDS = [
|
|
173
|
+
"empty",
|
|
174
|
+
"tom lista",
|
|
175
|
+
"inga poster",
|
|
176
|
+
"inga reser\xE4kningar",
|
|
177
|
+
"saknas data",
|
|
178
|
+
"no records",
|
|
179
|
+
"no expenses",
|
|
180
|
+
"no reports",
|
|
181
|
+
"missing data",
|
|
182
|
+
"unseeded"
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// src/eval/gemini-eval-cli.ts
|
|
188
|
+
init_walkthrough_evaluator();
|
|
189
|
+
import { existsSync, promises as fs2 } from "fs";
|
|
190
|
+
import http from "http";
|
|
191
|
+
import os from "os";
|
|
192
|
+
import path2 from "path";
|
|
193
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
194
|
+
import { GoogleAuth as GoogleAuth2 } from "google-auth-library";
|
|
195
|
+
|
|
196
|
+
// src/eval/gemini-walker.ts
|
|
197
|
+
import { execFile } from "child_process";
|
|
198
|
+
import { promises as fs } from "fs";
|
|
199
|
+
import path from "path";
|
|
200
|
+
import { promisify } from "util";
|
|
201
|
+
import { fileURLToPath } from "url";
|
|
202
|
+
import { GoogleAuth } from "google-auth-library";
|
|
203
|
+
var execFileAsync = promisify(execFile);
|
|
204
|
+
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
205
|
+
var GEMINI_TOOLS = [
|
|
206
|
+
{
|
|
207
|
+
functionDeclarations: [
|
|
208
|
+
{
|
|
209
|
+
name: "browse_action",
|
|
210
|
+
description: "Interact with the web browser surface: click, fill, select, or snapshot the page. Read ARIA roles and names from previous snapshots. NOTE: Call record_step to record meaningful persona steps and observations.",
|
|
211
|
+
parameters: {
|
|
212
|
+
type: "OBJECT",
|
|
213
|
+
properties: {
|
|
214
|
+
action: {
|
|
215
|
+
type: "STRING",
|
|
216
|
+
enum: [
|
|
217
|
+
"click",
|
|
218
|
+
"fill",
|
|
219
|
+
"select",
|
|
220
|
+
"press",
|
|
221
|
+
"scroll",
|
|
222
|
+
"back",
|
|
223
|
+
"snapshot"
|
|
224
|
+
],
|
|
225
|
+
description: "The browser action verb to perform"
|
|
226
|
+
},
|
|
227
|
+
role: {
|
|
228
|
+
type: "STRING",
|
|
229
|
+
description: "ARIA role of target element (e.g. 'button', 'link', 'combobox', 'textbox')"
|
|
230
|
+
},
|
|
231
|
+
name: {
|
|
232
|
+
type: "STRING",
|
|
233
|
+
description: "Exact or closest accessible name from the snapshot (e.g. 'Ny reser\xE4kning', 'Till', 'Fr\xE5n')"
|
|
234
|
+
},
|
|
235
|
+
value: {
|
|
236
|
+
type: "STRING",
|
|
237
|
+
description: "Text to type into an input, or option text to choose in a select/combobox"
|
|
238
|
+
},
|
|
239
|
+
nth: {
|
|
240
|
+
type: "INTEGER",
|
|
241
|
+
description: "0-based index if multiple elements match the same role or name"
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
required: ["action"]
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "record_step",
|
|
249
|
+
description: "Log an official step for this persona walkthrough report documenting actions and observations.",
|
|
250
|
+
parameters: {
|
|
251
|
+
type: "OBJECT",
|
|
252
|
+
properties: {
|
|
253
|
+
action: {
|
|
254
|
+
type: "STRING",
|
|
255
|
+
description: "What action the persona took (e.g. 'Selected Gothenburg destination')"
|
|
256
|
+
},
|
|
257
|
+
note: {
|
|
258
|
+
type: "STRING",
|
|
259
|
+
description: "Persona's internal thoughts, reasons, and observations (e.g. observed missing options, evaluated rates)"
|
|
260
|
+
},
|
|
261
|
+
surface: {
|
|
262
|
+
type: "STRING",
|
|
263
|
+
description: "UI section or surface name (e.g. 'Expense Form', 'Trip List', 'Login Portal')"
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
required: ["action", "note"]
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "record_finding",
|
|
271
|
+
description: "Record a genuine product defect, regulatory compliance violation, or usability barrier discovered during the walk.",
|
|
272
|
+
parameters: {
|
|
273
|
+
type: "OBJECT",
|
|
274
|
+
properties: {
|
|
275
|
+
title: {
|
|
276
|
+
type: "STRING",
|
|
277
|
+
description: "Clear defect title summarizing what is wrong"
|
|
278
|
+
},
|
|
279
|
+
type: {
|
|
280
|
+
type: "STRING",
|
|
281
|
+
enum: ["missing", "incorrect", "hinders", "helps", "irrelevant"],
|
|
282
|
+
description: "Defect taxonomy type"
|
|
283
|
+
},
|
|
284
|
+
severity: {
|
|
285
|
+
type: "STRING",
|
|
286
|
+
enum: ["critical", "major", "minor", "observation"],
|
|
287
|
+
description: "Severity rating"
|
|
288
|
+
},
|
|
289
|
+
consequence: {
|
|
290
|
+
type: "STRING",
|
|
291
|
+
description: "Impact on persona tasks or regulatory risk (e.g. Skatteverket tax compliance penalty)"
|
|
292
|
+
},
|
|
293
|
+
ui_fact: {
|
|
294
|
+
type: "STRING",
|
|
295
|
+
description: "Direct factual description of what the UI displays or is missing"
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
required: ["title", "type", "severity", "consequence", "ui_fact"]
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: "complete_walkthrough",
|
|
303
|
+
description: "Conclude the walkthrough when outline objectives are complete, or when unseeded preconditions halt testing.",
|
|
304
|
+
parameters: {
|
|
305
|
+
type: "OBJECT",
|
|
306
|
+
properties: {
|
|
307
|
+
summary: {
|
|
308
|
+
type: "STRING",
|
|
309
|
+
description: "Final wrap-up reasoning for concluding the walkthrough"
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
];
|
|
317
|
+
async function runGeminiWalkthrough(opts) {
|
|
318
|
+
const apiKey = opts.apiKey ?? process.env.GEMINI_API_KEY;
|
|
319
|
+
const project = opts.googleCloudProject ?? process.env.GOOGLE_CLOUD_PROJECT ?? "plasma-inquiry-492121-f7";
|
|
320
|
+
const location = opts.googleCloudLocation ?? process.env.GOOGLE_CLOUD_LOCATION ?? "global";
|
|
321
|
+
const model = opts.model ?? "gemini-2.5-flash";
|
|
322
|
+
const maxTurns = opts.maxTurns ?? 12;
|
|
323
|
+
let requestUrl;
|
|
324
|
+
let getHeaders;
|
|
325
|
+
if (apiKey) {
|
|
326
|
+
requestUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
327
|
+
getHeaders = async () => ({ "Content-Type": "application/json" });
|
|
328
|
+
} else {
|
|
329
|
+
const auth = new GoogleAuth({
|
|
330
|
+
scopes: "https://www.googleapis.com/auth/cloud-platform"
|
|
331
|
+
});
|
|
332
|
+
const client = await auth.getClient();
|
|
333
|
+
const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
|
|
334
|
+
requestUrl = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${model}:generateContent`;
|
|
335
|
+
getHeaders = async () => {
|
|
336
|
+
const token = (await client.getAccessToken()).token;
|
|
337
|
+
if (!token) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
"Vertex AI authentication failed: GCP client returned empty access token. Run 'gcloud auth application-default login' to authenticate."
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
Authorization: `Bearer ${token}`,
|
|
344
|
+
"Content-Type": "application/json"
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const runCli = async (args) => {
|
|
349
|
+
return execFileAsync(process.execPath, [opts.binPath, ...args], {
|
|
350
|
+
cwd: opts.projectDir,
|
|
351
|
+
env: {
|
|
352
|
+
...process.env,
|
|
353
|
+
NODE_ENV: "test",
|
|
354
|
+
IAMU_HOME: path.join(opts.projectDir, ".iamu")
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
const { stdout: startOut } = await runCli([
|
|
359
|
+
"start-run",
|
|
360
|
+
opts.scenarioSlug,
|
|
361
|
+
"--target",
|
|
362
|
+
opts.targetUrl,
|
|
363
|
+
"--no-check",
|
|
364
|
+
"--json"
|
|
365
|
+
]);
|
|
366
|
+
const { run_id: runId } = JSON.parse(startOut);
|
|
367
|
+
await runCli([
|
|
368
|
+
"browse",
|
|
369
|
+
"start",
|
|
370
|
+
"--headless",
|
|
371
|
+
"--port",
|
|
372
|
+
String(opts.cdpPort),
|
|
373
|
+
"--run",
|
|
374
|
+
runId,
|
|
375
|
+
"--json"
|
|
376
|
+
]);
|
|
377
|
+
await runCli(["browse", "goto", opts.targetUrl, "--json"]);
|
|
378
|
+
const { stdout: initialSnapOut } = await runCli([
|
|
379
|
+
"browse",
|
|
380
|
+
"snapshot",
|
|
381
|
+
"--json"
|
|
382
|
+
]);
|
|
383
|
+
const initialSnapData = JSON.parse(initialSnapOut);
|
|
384
|
+
let currentSnapshot = initialSnapData.snapshot ?? "";
|
|
385
|
+
let currentUrl = initialSnapData.url ?? opts.targetUrl;
|
|
386
|
+
const { stdout: promptOut } = await runCli([
|
|
387
|
+
"instructions",
|
|
388
|
+
"walk",
|
|
389
|
+
"--scenario",
|
|
390
|
+
opts.scenarioSlug,
|
|
391
|
+
"--run",
|
|
392
|
+
runId,
|
|
393
|
+
"--json"
|
|
394
|
+
]);
|
|
395
|
+
const parsedPrompt = JSON.parse(promptOut);
|
|
396
|
+
const systemInstruction = parsedPrompt.walker_prompt ?? parsedPrompt.prompt ?? "";
|
|
397
|
+
const contents = [
|
|
398
|
+
{
|
|
399
|
+
role: "user",
|
|
400
|
+
parts: [
|
|
401
|
+
{
|
|
402
|
+
text: `You are starting your persona walkthrough now.
|
|
403
|
+
|
|
404
|
+
Initial URL: ${currentUrl}
|
|
405
|
+
|
|
406
|
+
Initial Page ARIA Snapshot:
|
|
407
|
+
\`\`\`yaml
|
|
408
|
+
${currentSnapshot}
|
|
409
|
+
\`\`\`
|
|
410
|
+
|
|
411
|
+
Take your first action using the available tools. Inspect roles, click, fill, record steps, record all distinct defects found across all form sections, and call complete_walkthrough when the journey is finished.`
|
|
412
|
+
}
|
|
413
|
+
]
|
|
414
|
+
}
|
|
415
|
+
];
|
|
416
|
+
let isCompleted = false;
|
|
417
|
+
let turns = 0;
|
|
418
|
+
while (!isCompleted && turns < maxTurns) {
|
|
419
|
+
turns++;
|
|
420
|
+
let response = null;
|
|
421
|
+
for (let attempt = 1; attempt <= 4; attempt++) {
|
|
422
|
+
const headers = await getHeaders();
|
|
423
|
+
response = await fetch(requestUrl, {
|
|
424
|
+
method: "POST",
|
|
425
|
+
headers,
|
|
426
|
+
body: JSON.stringify({
|
|
427
|
+
systemInstruction: {
|
|
428
|
+
parts: [{ text: systemInstruction }]
|
|
429
|
+
},
|
|
430
|
+
contents,
|
|
431
|
+
tools: GEMINI_TOOLS,
|
|
432
|
+
generationConfig: {
|
|
433
|
+
temperature: 0.1
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
});
|
|
437
|
+
if (response.status === 429 && attempt < 4) {
|
|
438
|
+
const delay = attempt * 3e3;
|
|
439
|
+
console.log(
|
|
440
|
+
` [Rate Limit 429] Retrying turn ${turns} in ${delay}ms...`
|
|
441
|
+
);
|
|
442
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
if (!response || !response.ok) {
|
|
448
|
+
const errBody = response ? await response.text() : "No response";
|
|
449
|
+
throw new Error(
|
|
450
|
+
`Gemini API error (${response?.status ?? 500}): ${errBody}`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
const data = await response.json();
|
|
454
|
+
const candidate = data.candidates?.[0]?.content;
|
|
455
|
+
if (!candidate || !candidate.parts) {
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
contents.push({
|
|
459
|
+
role: "model",
|
|
460
|
+
parts: candidate.parts.map((p) => {
|
|
461
|
+
const thoughtSig = "thoughtSignature" in p && typeof p.thoughtSignature === "string" ? p.thoughtSignature : void 0;
|
|
462
|
+
if ("functionCall" in p && p.functionCall) {
|
|
463
|
+
return {
|
|
464
|
+
functionCall: p.functionCall,
|
|
465
|
+
...thoughtSig ? { thoughtSignature: thoughtSig } : {}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
text: "text" in p && p.text ? p.text : "",
|
|
470
|
+
...thoughtSig ? { thoughtSignature: thoughtSig } : {}
|
|
471
|
+
};
|
|
472
|
+
})
|
|
473
|
+
});
|
|
474
|
+
const toolCalls = candidate.parts.filter(
|
|
475
|
+
(p) => "functionCall" in p && Boolean(p.functionCall)
|
|
476
|
+
);
|
|
477
|
+
if (toolCalls.length === 0) {
|
|
478
|
+
contents.push({
|
|
479
|
+
role: "user",
|
|
480
|
+
parts: [
|
|
481
|
+
{
|
|
482
|
+
text: "Please use the tools (browse_action, record_step, record_finding, complete_walkthrough) to continue driving the walkthrough."
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
});
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
console.log(
|
|
489
|
+
` [Turn ${turns}] ${toolCalls.map(
|
|
490
|
+
(t) => `${t.functionCall.name}(${JSON.stringify(t.functionCall.args)})`
|
|
491
|
+
).join(", ")}`
|
|
492
|
+
);
|
|
493
|
+
const toolResponses = [];
|
|
494
|
+
for (const { functionCall } of toolCalls) {
|
|
495
|
+
const { name, args } = functionCall;
|
|
496
|
+
if (name === "browse_action") {
|
|
497
|
+
const action = String(args.action ?? "snapshot");
|
|
498
|
+
let role = args.role ? String(args.role) : void 0;
|
|
499
|
+
let targetName = args.name ? String(args.name) : void 0;
|
|
500
|
+
const value = args.value ? String(args.value) : void 0;
|
|
501
|
+
if (action === "click") {
|
|
502
|
+
if (!role && targetName) {
|
|
503
|
+
role = "button";
|
|
504
|
+
} else if (!targetName && role) {
|
|
505
|
+
targetName = role;
|
|
506
|
+
role = "button";
|
|
507
|
+
}
|
|
508
|
+
} else if (action === "fill") {
|
|
509
|
+
if (!role && targetName) {
|
|
510
|
+
role = "textbox";
|
|
511
|
+
}
|
|
512
|
+
} else if (action === "select") {
|
|
513
|
+
if (!role && targetName) {
|
|
514
|
+
role = "combobox";
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const cliArgs = ["browse", action];
|
|
518
|
+
if (action === "goto") {
|
|
519
|
+
cliArgs.push(value ?? targetName ?? opts.targetUrl);
|
|
520
|
+
} else {
|
|
521
|
+
if (role && targetName) {
|
|
522
|
+
cliArgs.push(role, targetName);
|
|
523
|
+
}
|
|
524
|
+
if (value) {
|
|
525
|
+
cliArgs.push(value);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (args.nth !== void 0) {
|
|
529
|
+
cliArgs.push("--nth", String(args.nth));
|
|
530
|
+
}
|
|
531
|
+
cliArgs.push("--json");
|
|
532
|
+
try {
|
|
533
|
+
const { stdout: actionOut } = await runCli(cliArgs);
|
|
534
|
+
const parsed = JSON.parse(actionOut);
|
|
535
|
+
currentUrl = parsed.url ?? currentUrl;
|
|
536
|
+
const { stdout: snapOut } = await runCli([
|
|
537
|
+
"browse",
|
|
538
|
+
"snapshot",
|
|
539
|
+
"--json"
|
|
540
|
+
]);
|
|
541
|
+
const snapObj = JSON.parse(snapOut);
|
|
542
|
+
currentSnapshot = snapObj.snapshot ?? "";
|
|
543
|
+
toolResponses.push({
|
|
544
|
+
functionResponse: {
|
|
545
|
+
name,
|
|
546
|
+
response: {
|
|
547
|
+
status: "success",
|
|
548
|
+
url: currentUrl,
|
|
549
|
+
snapshot: currentSnapshot,
|
|
550
|
+
instruction: "Action executed. If this was a meaningful step in your scenario walkthrough, call record_step to record your observation."
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
} catch (err) {
|
|
555
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
556
|
+
console.log(` -> Action error: ${errMsg.slice(0, 120)}`);
|
|
557
|
+
toolResponses.push({
|
|
558
|
+
functionResponse: {
|
|
559
|
+
name,
|
|
560
|
+
response: {
|
|
561
|
+
status: "error",
|
|
562
|
+
message: errMsg,
|
|
563
|
+
snapshot: currentSnapshot
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
} else if (name === "record_step") {
|
|
569
|
+
const stepAction = String(args.action ?? "");
|
|
570
|
+
const stepNote = String(args.note ?? "");
|
|
571
|
+
const surface = String(args.surface ?? "Page");
|
|
572
|
+
const cliArgs = [
|
|
573
|
+
"step",
|
|
574
|
+
runId,
|
|
575
|
+
"--action",
|
|
576
|
+
stepAction,
|
|
577
|
+
"--note",
|
|
578
|
+
stepNote,
|
|
579
|
+
"--url",
|
|
580
|
+
currentUrl,
|
|
581
|
+
"--surface",
|
|
582
|
+
surface,
|
|
583
|
+
"--json"
|
|
584
|
+
];
|
|
585
|
+
try {
|
|
586
|
+
const { stdout: stepOut } = await runCli(cliArgs);
|
|
587
|
+
toolResponses.push({
|
|
588
|
+
functionResponse: {
|
|
589
|
+
name,
|
|
590
|
+
response: {
|
|
591
|
+
status: "success",
|
|
592
|
+
result: JSON.parse(stepOut)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
} catch (err) {
|
|
597
|
+
toolResponses.push({
|
|
598
|
+
functionResponse: {
|
|
599
|
+
name,
|
|
600
|
+
response: {
|
|
601
|
+
status: "error",
|
|
602
|
+
message: err instanceof Error ? err.message : String(err)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
} else if (name === "record_finding") {
|
|
608
|
+
const title = String(args.title ?? "");
|
|
609
|
+
const type = String(args.type ?? "missing");
|
|
610
|
+
const severity = String(args.severity ?? "major");
|
|
611
|
+
const consequence = String(args.consequence ?? "");
|
|
612
|
+
const uiFact = String(args.ui_fact ?? "");
|
|
613
|
+
const cliArgs = [
|
|
614
|
+
"new-finding",
|
|
615
|
+
"--run",
|
|
616
|
+
runId,
|
|
617
|
+
"--scenario",
|
|
618
|
+
opts.scenarioSlug,
|
|
619
|
+
"--title",
|
|
620
|
+
title,
|
|
621
|
+
"--type",
|
|
622
|
+
type,
|
|
623
|
+
"--severity",
|
|
624
|
+
severity,
|
|
625
|
+
"--consequence",
|
|
626
|
+
consequence,
|
|
627
|
+
"--ui-fact",
|
|
628
|
+
uiFact
|
|
629
|
+
];
|
|
630
|
+
if (args.task_impact) {
|
|
631
|
+
cliArgs.push("--task-impact", String(args.task_impact));
|
|
632
|
+
}
|
|
633
|
+
if (args.confidence) {
|
|
634
|
+
cliArgs.push("--confidence", String(args.confidence));
|
|
635
|
+
}
|
|
636
|
+
cliArgs.push("--json");
|
|
637
|
+
try {
|
|
638
|
+
const { stdout: findingOut } = await runCli(cliArgs);
|
|
639
|
+
console.log(
|
|
640
|
+
` -> record_finding success: ${findingOut.trim().slice(0, 140)}`
|
|
641
|
+
);
|
|
642
|
+
toolResponses.push({
|
|
643
|
+
functionResponse: {
|
|
644
|
+
name,
|
|
645
|
+
response: {
|
|
646
|
+
status: "success",
|
|
647
|
+
finding: JSON.parse(findingOut),
|
|
648
|
+
instruction: "Finding successfully logged. Do not terminate the walkthrough early \u2014 continue through the remaining form sections (including per diem meal deductions and expenses) to record all distinct compliance defects and complete the journey."
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
} catch (err) {
|
|
653
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
654
|
+
console.log(` -> record_finding error: ${errMsg}`);
|
|
655
|
+
toolResponses.push({
|
|
656
|
+
functionResponse: {
|
|
657
|
+
name,
|
|
658
|
+
response: {
|
|
659
|
+
status: "error",
|
|
660
|
+
message: errMsg
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
} else if (name === "complete_walkthrough") {
|
|
666
|
+
isCompleted = true;
|
|
667
|
+
try {
|
|
668
|
+
await runCli(["complete", runId, "--yes", "--no-prompt", "--json"]);
|
|
669
|
+
toolResponses.push({
|
|
670
|
+
functionResponse: {
|
|
671
|
+
name,
|
|
672
|
+
response: {
|
|
673
|
+
status: "success",
|
|
674
|
+
completed: true,
|
|
675
|
+
run_id: runId
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
} catch (err) {
|
|
680
|
+
toolResponses.push({
|
|
681
|
+
functionResponse: {
|
|
682
|
+
name,
|
|
683
|
+
response: {
|
|
684
|
+
status: "error",
|
|
685
|
+
message: err instanceof Error ? err.message : String(err)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
contents.push({
|
|
693
|
+
role: "user",
|
|
694
|
+
parts: toolResponses
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
await runCli(["browse", "stop", "--json"]);
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
701
|
+
if (!isCompleted) {
|
|
702
|
+
try {
|
|
703
|
+
await runCli(["complete", runId, "--yes", "--no-prompt", "--json"]);
|
|
704
|
+
} catch {
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const canonicalFile = path.join(
|
|
708
|
+
opts.projectDir,
|
|
709
|
+
"iamu",
|
|
710
|
+
".runs",
|
|
711
|
+
runId,
|
|
712
|
+
"canonical.json"
|
|
713
|
+
);
|
|
714
|
+
let canonicalData;
|
|
715
|
+
try {
|
|
716
|
+
canonicalData = JSON.parse(await fs.readFile(canonicalFile, "utf-8"));
|
|
717
|
+
} catch {
|
|
718
|
+
const fallbackFile = path.join(
|
|
719
|
+
opts.projectDir,
|
|
720
|
+
"iamu",
|
|
721
|
+
".runs",
|
|
722
|
+
runId,
|
|
723
|
+
"walkthrough.json"
|
|
724
|
+
);
|
|
725
|
+
canonicalData = JSON.parse(await fs.readFile(fallbackFile, "utf-8"));
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
runId,
|
|
729
|
+
canonical: canonicalData,
|
|
730
|
+
turns
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/eval/gemini-eval-cli.ts
|
|
735
|
+
var __dirname2 = path2.dirname(fileURLToPath2(import.meta.url));
|
|
736
|
+
var BENCHMARK_DIR = path2.resolve(
|
|
737
|
+
__dirname2,
|
|
738
|
+
"..",
|
|
739
|
+
"..",
|
|
740
|
+
"..",
|
|
741
|
+
"..",
|
|
742
|
+
"tests",
|
|
743
|
+
"benchmarks",
|
|
744
|
+
"reserakning"
|
|
745
|
+
);
|
|
746
|
+
var BENCHMARK_SCENARIOS = [
|
|
747
|
+
{
|
|
748
|
+
slug: "submit-gothenburg-client-trip",
|
|
749
|
+
queryParam: "",
|
|
750
|
+
contract: {
|
|
751
|
+
scenarioSlug: "submit-gothenburg-client-trip",
|
|
752
|
+
expectedState: "populated",
|
|
753
|
+
optimalSteps: 1,
|
|
754
|
+
expectedDefects: [
|
|
755
|
+
{
|
|
756
|
+
id: "def-departure-time",
|
|
757
|
+
category: "compliance",
|
|
758
|
+
keywords: ["departure", "arrival", "avresa", "tid"],
|
|
759
|
+
description: "Missing departure/arrival times for per diem"
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
id: "def-meal-deduction",
|
|
763
|
+
category: "compliance",
|
|
764
|
+
keywords: [
|
|
765
|
+
"meal deduction",
|
|
766
|
+
"traktamente",
|
|
767
|
+
"breakfast",
|
|
768
|
+
"kostavdrag"
|
|
769
|
+
],
|
|
770
|
+
description: "Missing meal deductions and outdated rate"
|
|
771
|
+
}
|
|
772
|
+
]
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
slug: "audit-empty-queue",
|
|
777
|
+
queryParam: "?state=empty",
|
|
778
|
+
contract: {
|
|
779
|
+
scenarioSlug: "audit-empty-queue",
|
|
780
|
+
expectedState: "empty",
|
|
781
|
+
optimalSteps: 1
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
slug: "submit-malmo-client-trip",
|
|
786
|
+
queryParam: "?state=mismatch",
|
|
787
|
+
contract: {
|
|
788
|
+
scenarioSlug: "submit-malmo-client-trip",
|
|
789
|
+
expectedState: "mismatch",
|
|
790
|
+
optimalSteps: 1,
|
|
791
|
+
expectedApproximationTarget: "G\xF6teborg",
|
|
792
|
+
allowedSurfaces: [
|
|
793
|
+
"Expense Form",
|
|
794
|
+
"Mina reser\xE4kningar",
|
|
795
|
+
"Ny reser\xE4kning",
|
|
796
|
+
"Form",
|
|
797
|
+
"Dashboard",
|
|
798
|
+
"New Expense Report",
|
|
799
|
+
"Reser\xE4kning",
|
|
800
|
+
"Reseinformation",
|
|
801
|
+
"Page",
|
|
802
|
+
"Formul\xE4r",
|
|
803
|
+
"Utl\xE4gg",
|
|
804
|
+
"Traktamente"
|
|
805
|
+
]
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
slug: "submit-gated-client-trip",
|
|
810
|
+
queryParam: "?auth=required",
|
|
811
|
+
contract: {
|
|
812
|
+
scenarioSlug: "submit-gated-client-trip",
|
|
813
|
+
expectedState: "auth_gated",
|
|
814
|
+
optimalSteps: 2,
|
|
815
|
+
authRequired: true
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
slug: "submit-trip-methodical-esg",
|
|
820
|
+
queryParam: "",
|
|
821
|
+
contract: {
|
|
822
|
+
scenarioSlug: "submit-trip-methodical-esg",
|
|
823
|
+
expectedState: "populated",
|
|
824
|
+
optimalSteps: 2,
|
|
825
|
+
expectedDefects: [
|
|
826
|
+
{
|
|
827
|
+
id: "def-departure-time",
|
|
828
|
+
category: "compliance",
|
|
829
|
+
keywords: ["departure", "arrival", "avresa", "tid"],
|
|
830
|
+
description: "Missing departure/arrival times for per diem"
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
id: "def-meal-deduction",
|
|
834
|
+
category: "compliance",
|
|
835
|
+
keywords: [
|
|
836
|
+
"meal deduction",
|
|
837
|
+
"traktamente",
|
|
838
|
+
"breakfast",
|
|
839
|
+
"kostavdrag"
|
|
840
|
+
],
|
|
841
|
+
description: "Missing meal deductions and outdated rate"
|
|
842
|
+
}
|
|
843
|
+
]
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
];
|
|
847
|
+
async function main() {
|
|
848
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
849
|
+
let authDescription = "Google AI Studio (API Key)";
|
|
850
|
+
if (!apiKey) {
|
|
851
|
+
try {
|
|
852
|
+
const auth = new GoogleAuth2({
|
|
853
|
+
scopes: "https://www.googleapis.com/auth/cloud-platform"
|
|
854
|
+
});
|
|
855
|
+
const client = await auth.getClient();
|
|
856
|
+
const tokenRes = await client.getAccessToken();
|
|
857
|
+
if (!tokenRes.token) {
|
|
858
|
+
throw new Error("Empty access token");
|
|
859
|
+
}
|
|
860
|
+
const project = process.env.GOOGLE_CLOUD_PROJECT || "plasma-inquiry-492121-f7";
|
|
861
|
+
const location = process.env.GOOGLE_CLOUD_LOCATION || "global";
|
|
862
|
+
authDescription = `Google Cloud Vertex AI (project: ${project}, location: ${location})`;
|
|
863
|
+
} catch {
|
|
864
|
+
console.error(`
|
|
865
|
+
\x1B[31m[ERROR] No Gemini credentials found.\x1B[0m
|
|
866
|
+
|
|
867
|
+
To run autonomous LLM walker evaluations with Gemini 2.5 Flash, either:
|
|
868
|
+
1. Authenticate with Google Cloud Vertex AI (Recommended):
|
|
869
|
+
gcloud auth application-default login
|
|
870
|
+
2. Or provide an AI Studio API key:
|
|
871
|
+
GEMINI_API_KEY=your_key_here pnpm eval:gemini
|
|
872
|
+
`);
|
|
873
|
+
process.exit(1);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const binPath = path2.resolve(__dirname2, "..", "bin.js");
|
|
877
|
+
if (!existsSync(binPath)) {
|
|
878
|
+
console.error(
|
|
879
|
+
"\x1B[31m[ERROR] CLI binary not built. Run 'pnpm build' first.\x1B[0m"
|
|
880
|
+
);
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
const htmlContent = await fs2.readFile(
|
|
884
|
+
path2.join(BENCHMARK_DIR, "index.html"),
|
|
885
|
+
"utf-8"
|
|
886
|
+
);
|
|
887
|
+
const htmlBuffer = Buffer.from(htmlContent, "utf-8");
|
|
888
|
+
const server = http.createServer((req, res) => {
|
|
889
|
+
if (req.url === "/favicon.ico") {
|
|
890
|
+
res.writeHead(204);
|
|
891
|
+
res.end();
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
res.writeHead(200, {
|
|
895
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
896
|
+
"Content-Length": htmlBuffer.length
|
|
897
|
+
});
|
|
898
|
+
res.end(htmlBuffer);
|
|
899
|
+
});
|
|
900
|
+
const serverPort = 31234;
|
|
901
|
+
await new Promise((resolve) => {
|
|
902
|
+
server.listen(serverPort, "127.0.0.1", () => resolve());
|
|
903
|
+
});
|
|
904
|
+
const filterArg = process.argv.slice(2).find((a) => !a.startsWith("-"));
|
|
905
|
+
const scenariosToRun = filterArg ? BENCHMARK_SCENARIOS.filter((s) => s.slug.includes(filterArg)) : BENCHMARK_SCENARIOS;
|
|
906
|
+
if (scenariosToRun.length === 0) {
|
|
907
|
+
console.error(
|
|
908
|
+
`\x1B[31m[ERROR] No scenarios matched filter: "${filterArg}"\x1B[0m`
|
|
909
|
+
);
|
|
910
|
+
process.exit(1);
|
|
911
|
+
}
|
|
912
|
+
console.log(
|
|
913
|
+
`
|
|
914
|
+
\x1B[36m=== Autonomous Gemini Walker Benchmark Suite ===\x1B[0m`
|
|
915
|
+
);
|
|
916
|
+
console.log(`Provider: ${authDescription}`);
|
|
917
|
+
console.log(`Model: gemini-2.5-flash`);
|
|
918
|
+
console.log(`Local Fixture: http://127.0.0.1:${serverPort}`);
|
|
919
|
+
console.log(
|
|
920
|
+
`Scenarios: ${scenariosToRun.length}${filterArg ? ` (filtered by "${filterArg}")` : ""}
|
|
921
|
+
`
|
|
922
|
+
);
|
|
923
|
+
const scorecards = [];
|
|
924
|
+
let cdpPortCounter = 19550;
|
|
925
|
+
try {
|
|
926
|
+
for (const { slug, queryParam, contract } of scenariosToRun) {
|
|
927
|
+
console.log(
|
|
928
|
+
`\x1B[33m\u25B6 Running autonomous walker for scenario:\x1B[0m ${slug}...`
|
|
929
|
+
);
|
|
930
|
+
const tmpDir = await fs2.mkdtemp(
|
|
931
|
+
path2.join(os.tmpdir(), `iamu-eval-gemini-${slug}-`)
|
|
932
|
+
);
|
|
933
|
+
await fs2.cp(path2.join(BENCHMARK_DIR, "iamu"), path2.join(tmpDir, "iamu"), {
|
|
934
|
+
recursive: true
|
|
935
|
+
});
|
|
936
|
+
const cdpPort = cdpPortCounter++;
|
|
937
|
+
const targetUrl = `http://127.0.0.1:${serverPort}/${queryParam}`;
|
|
938
|
+
try {
|
|
939
|
+
const { canonical, turns } = await runGeminiWalkthrough({
|
|
940
|
+
scenarioSlug: slug,
|
|
941
|
+
targetUrl,
|
|
942
|
+
projectDir: tmpDir,
|
|
943
|
+
binPath,
|
|
944
|
+
cdpPort,
|
|
945
|
+
apiKey,
|
|
946
|
+
model: "gemini-2.5-flash",
|
|
947
|
+
maxTurns: 24
|
|
948
|
+
});
|
|
949
|
+
const { evaluateWalkthroughRun: evaluateWalkthroughRun2 } = await Promise.resolve().then(() => (init_walkthrough_evaluator(), walkthrough_evaluator_exports));
|
|
950
|
+
const scorecard = evaluateWalkthroughRun2(canonical, contract);
|
|
951
|
+
scorecards.push(scorecard);
|
|
952
|
+
console.log(
|
|
953
|
+
` \x1B[32m\u2713\x1B[0m Completed in ${turns} turns. Verdict: ${scorecard.passed ? "\x1B[32mPASS\x1B[0m" : "\x1B[31mFAIL\x1B[0m"} - ${scorecard.summary}
|
|
954
|
+
`
|
|
955
|
+
);
|
|
956
|
+
} catch (err) {
|
|
957
|
+
console.error(
|
|
958
|
+
` \x1B[31m\u2717\x1B[0m Run error:`,
|
|
959
|
+
err instanceof Error ? err.message : String(err)
|
|
960
|
+
);
|
|
961
|
+
} finally {
|
|
962
|
+
await fs2.rm(tmpDir, { recursive: true, force: true });
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (scorecards.length > 0) {
|
|
966
|
+
console.log(formatEvaluationScorecardTable(scorecards));
|
|
967
|
+
}
|
|
968
|
+
} finally {
|
|
969
|
+
server.closeAllConnections?.();
|
|
970
|
+
server.close();
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
main().catch((err) => {
|
|
974
|
+
console.error("Fatal error running Gemini walker benchmark:", err);
|
|
975
|
+
process.exit(1);
|
|
976
|
+
});
|