@browserbasehq/orca 3.5.1-preview.0 → 3.6.0-gemini-cua
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 +0 -12
- package/dist/cjs/lib/utils.d.ts +6 -0
- package/dist/cjs/lib/utils.js +11 -0
- package/dist/cjs/lib/utils.js.map +1 -1
- package/dist/cjs/lib/v3/agent/AgentProvider.js +4 -3
- package/dist/cjs/lib/v3/agent/AgentProvider.js.map +1 -1
- package/dist/cjs/lib/v3/agent/AnthropicCUAClient.js +10 -9
- package/dist/cjs/lib/v3/agent/AnthropicCUAClient.js.map +1 -1
- package/dist/cjs/lib/v3/agent/GoogleCUAClient.js +124 -28
- package/dist/cjs/lib/v3/agent/GoogleCUAClient.js.map +1 -1
- package/dist/cjs/lib/v3/agent/utils/handleDoneToolCall.js +10 -1
- package/dist/cjs/lib/v3/agent/utils/handleDoneToolCall.js.map +1 -1
- package/dist/cjs/lib/v3/cache/ActCache.js +1 -1
- package/dist/cjs/lib/v3/cache/ActCache.js.map +1 -1
- package/dist/cjs/lib/v3/cache/utils.d.ts +7 -0
- package/dist/cjs/lib/v3/cache/utils.js +19 -0
- package/dist/cjs/lib/v3/cache/utils.js.map +1 -1
- package/dist/cjs/lib/v3/handlers/v3AgentHandler.d.ts +2 -1
- package/dist/cjs/lib/v3/handlers/v3AgentHandler.js +25 -9
- package/dist/cjs/lib/v3/handlers/v3AgentHandler.js.map +1 -1
- package/dist/cjs/lib/v3/index.d.ts +2 -2
- package/dist/cjs/lib/v3/index.js.map +1 -1
- package/dist/cjs/lib/v3/launch/local.js +1 -0
- package/dist/cjs/lib/v3/launch/local.js.map +1 -1
- package/dist/cjs/lib/v3/llm/LLMProvider.js +1 -0
- package/dist/cjs/lib/v3/llm/LLMProvider.js.map +1 -1
- package/dist/cjs/lib/v3/llm/aisdk.js +4 -0
- package/dist/cjs/lib/v3/llm/aisdk.js.map +1 -1
- package/dist/cjs/lib/v3/llm/anthropicOptions.d.ts +57 -0
- package/dist/cjs/lib/v3/llm/anthropicOptions.js +115 -0
- package/dist/cjs/lib/v3/llm/anthropicOptions.js.map +1 -0
- package/dist/cjs/lib/v3/types/public/agent.d.ts +1 -1
- package/dist/cjs/lib/v3/types/public/agent.js +2 -0
- package/dist/cjs/lib/v3/types/public/agent.js.map +1 -1
- package/dist/cjs/lib/v3/types/public/model.d.ts +3 -2
- package/dist/cjs/lib/v3/types/public/model.js.map +1 -1
- package/dist/cjs/lib/v3/understudy/page.js +1 -1
- package/dist/cjs/lib/v3/understudy/page.js.map +1 -1
- package/dist/cjs/lib/v3/v3.js +4 -1
- package/dist/cjs/lib/v3/v3.js.map +1 -1
- package/dist/cjs/lib/v3/verifier/errorTaxonomy.d.ts +35 -0
- package/dist/cjs/lib/v3/verifier/errorTaxonomy.js +291 -0
- package/dist/cjs/lib/v3/verifier/errorTaxonomy.js.map +1 -0
- package/dist/cjs/lib/v3/verifier/evidence.d.ts +44 -0
- package/dist/cjs/lib/v3/verifier/evidence.js +432 -0
- package/dist/cjs/lib/v3/verifier/evidence.js.map +1 -0
- package/dist/cjs/lib/v3/verifier/index.d.ts +1 -1
- package/dist/cjs/lib/v3/verifier/index.js.map +1 -1
- package/dist/cjs/lib/v3/verifier/prompts/batchedRelevance.d.ts +21 -0
- package/dist/cjs/lib/v3/verifier/prompts/batchedRelevance.js +71 -0
- package/dist/cjs/lib/v3/verifier/prompts/batchedRelevance.js.map +1 -0
- package/dist/cjs/lib/v3/verifier/prompts/firstPointOfFailure.d.ts +3 -0
- package/dist/cjs/lib/v3/verifier/prompts/firstPointOfFailure.js +166 -0
- package/dist/cjs/lib/v3/verifier/prompts/firstPointOfFailure.js.map +1 -0
- package/dist/cjs/lib/v3/verifier/prompts/fusedJudgment.d.ts +35 -0
- package/dist/cjs/lib/v3/verifier/prompts/fusedJudgment.js +158 -0
- package/dist/cjs/lib/v3/verifier/prompts/fusedJudgment.js.map +1 -0
- package/dist/cjs/lib/v3/verifier/prompts/fusedOutcome.d.ts +26 -0
- package/dist/cjs/lib/v3/verifier/prompts/fusedOutcome.js +137 -0
- package/dist/cjs/lib/v3/verifier/prompts/fusedOutcome.js.map +1 -0
- package/dist/cjs/lib/v3/verifier/prompts/index.d.ts +8 -0
- package/dist/cjs/lib/v3/verifier/prompts/index.js +22 -0
- package/dist/cjs/lib/v3/verifier/prompts/index.js.map +1 -0
- package/dist/cjs/lib/v3/verifier/prompts/perCriterionScore.d.ts +23 -0
- package/dist/cjs/lib/v3/verifier/prompts/perCriterionScore.js +77 -0
- package/dist/cjs/lib/v3/verifier/prompts/perCriterionScore.js.map +1 -0
- package/dist/cjs/lib/v3/verifier/prompts/render.d.ts +23 -0
- package/dist/cjs/lib/v3/verifier/prompts/render.js +45 -0
- package/dist/cjs/lib/v3/verifier/prompts/render.js.map +1 -0
- package/dist/cjs/lib/v3/verifier/prompts/rubricGeneration.d.ts +16 -0
- package/dist/cjs/lib/v3/verifier/prompts/rubricGeneration.js +321 -0
- package/dist/cjs/lib/v3/verifier/prompts/rubricGeneration.js.map +1 -0
- package/dist/cjs/lib/v3/verifier/prompts/taskValidity.d.ts +1 -0
- package/dist/cjs/lib/v3/verifier/prompts/taskValidity.js +116 -0
- package/dist/cjs/lib/v3/verifier/prompts/taskValidity.js.map +1 -0
- package/dist/cjs/lib/v3/verifier/rubricVerifier.d.ts +91 -0
- package/dist/cjs/lib/v3/verifier/rubricVerifier.js +1645 -0
- package/dist/cjs/lib/v3/verifier/rubricVerifier.js.map +1 -0
- package/dist/cjs/lib/v3/verifier/trajectory.d.ts +3 -3
- package/dist/cjs/lib/v3/verifier/trajectory.js +26 -26
- package/dist/cjs/lib/v3/verifier/trajectory.js.map +1 -1
- package/dist/cjs/lib/v3/verifier/types.d.ts +163 -15
- package/dist/cjs/lib/v3/verifier/types.js +0 -7
- package/dist/cjs/lib/v3/verifier/types.js.map +1 -1
- package/dist/cjs/lib/v3Evaluator.d.ts +5 -0
- package/dist/cjs/lib/v3Evaluator.js +47 -7
- package/dist/cjs/lib/v3Evaluator.js.map +1 -1
- package/dist/cjs/lib/version.d.ts +1 -1
- package/dist/cjs/lib/version.js +1 -1
- package/dist/cjs/lib/version.js.map +1 -1
- package/dist/esm/lib/utils.d.ts +6 -0
- package/dist/esm/lib/utils.js +10 -0
- package/dist/esm/lib/utils.js.map +1 -1
- package/dist/esm/lib/v3/agent/AgentProvider.js +4 -3
- package/dist/esm/lib/v3/agent/AgentProvider.js.map +1 -1
- package/dist/esm/lib/v3/agent/AnthropicCUAClient.js +10 -9
- package/dist/esm/lib/v3/agent/AnthropicCUAClient.js.map +1 -1
- package/dist/esm/lib/v3/agent/GoogleCUAClient.js +124 -28
- package/dist/esm/lib/v3/agent/GoogleCUAClient.js.map +1 -1
- package/dist/esm/lib/v3/agent/utils/handleDoneToolCall.js +10 -1
- package/dist/esm/lib/v3/agent/utils/handleDoneToolCall.js.map +1 -1
- package/dist/esm/lib/v3/cache/ActCache.js +2 -2
- package/dist/esm/lib/v3/cache/ActCache.js.map +1 -1
- package/dist/esm/lib/v3/cache/utils.d.ts +7 -0
- package/dist/esm/lib/v3/cache/utils.js +18 -0
- package/dist/esm/lib/v3/cache/utils.js.map +1 -1
- package/dist/esm/lib/v3/handlers/v3AgentHandler.d.ts +2 -1
- package/dist/esm/lib/v3/handlers/v3AgentHandler.js +25 -9
- package/dist/esm/lib/v3/handlers/v3AgentHandler.js.map +1 -1
- package/dist/esm/lib/v3/index.d.ts +2 -2
- package/dist/esm/lib/v3/index.js.map +1 -1
- package/dist/esm/lib/v3/launch/local.js +1 -0
- package/dist/esm/lib/v3/launch/local.js.map +1 -1
- package/dist/esm/lib/v3/llm/LLMProvider.js +1 -0
- package/dist/esm/lib/v3/llm/LLMProvider.js.map +1 -1
- package/dist/esm/lib/v3/llm/aisdk.js +4 -0
- package/dist/esm/lib/v3/llm/aisdk.js.map +1 -1
- package/dist/esm/lib/v3/llm/anthropicOptions.d.ts +57 -0
- package/dist/esm/lib/v3/llm/anthropicOptions.js +106 -0
- package/dist/esm/lib/v3/llm/anthropicOptions.js.map +1 -0
- package/dist/esm/lib/v3/types/public/agent.d.ts +1 -1
- package/dist/esm/lib/v3/types/public/agent.js +2 -0
- package/dist/esm/lib/v3/types/public/agent.js.map +1 -1
- package/dist/esm/lib/v3/types/public/model.d.ts +3 -2
- package/dist/esm/lib/v3/types/public/model.js.map +1 -1
- package/dist/esm/lib/v3/understudy/page.js +1 -1
- package/dist/esm/lib/v3/understudy/page.js.map +1 -1
- package/dist/esm/lib/v3/v3.js +4 -1
- package/dist/esm/lib/v3/v3.js.map +1 -1
- package/dist/esm/lib/v3/verifier/errorTaxonomy.d.ts +35 -0
- package/dist/esm/lib/v3/verifier/errorTaxonomy.js +285 -0
- package/dist/esm/lib/v3/verifier/errorTaxonomy.js.map +1 -0
- package/dist/esm/lib/v3/verifier/evidence.d.ts +44 -0
- package/dist/esm/lib/v3/verifier/evidence.js +393 -0
- package/dist/esm/lib/v3/verifier/evidence.js.map +1 -0
- package/dist/esm/lib/v3/verifier/index.d.ts +1 -1
- package/dist/esm/lib/v3/verifier/index.js.map +1 -1
- package/dist/esm/lib/v3/verifier/prompts/batchedRelevance.d.ts +21 -0
- package/dist/esm/lib/v3/verifier/prompts/batchedRelevance.js +68 -0
- package/dist/esm/lib/v3/verifier/prompts/batchedRelevance.js.map +1 -0
- package/dist/esm/lib/v3/verifier/prompts/firstPointOfFailure.d.ts +3 -0
- package/dist/esm/lib/v3/verifier/prompts/firstPointOfFailure.js +162 -0
- package/dist/esm/lib/v3/verifier/prompts/firstPointOfFailure.js.map +1 -0
- package/dist/esm/lib/v3/verifier/prompts/fusedJudgment.d.ts +35 -0
- package/dist/esm/lib/v3/verifier/prompts/fusedJudgment.js +155 -0
- package/dist/esm/lib/v3/verifier/prompts/fusedJudgment.js.map +1 -0
- package/dist/esm/lib/v3/verifier/prompts/fusedOutcome.d.ts +26 -0
- package/dist/esm/lib/v3/verifier/prompts/fusedOutcome.js +134 -0
- package/dist/esm/lib/v3/verifier/prompts/fusedOutcome.js.map +1 -0
- package/dist/esm/lib/v3/verifier/prompts/index.d.ts +8 -0
- package/dist/esm/lib/v3/verifier/prompts/index.js +9 -0
- package/dist/esm/lib/v3/verifier/prompts/index.js.map +1 -0
- package/dist/esm/lib/v3/verifier/prompts/perCriterionScore.d.ts +23 -0
- package/dist/esm/lib/v3/verifier/prompts/perCriterionScore.js +74 -0
- package/dist/esm/lib/v3/verifier/prompts/perCriterionScore.js.map +1 -0
- package/dist/esm/lib/v3/verifier/prompts/render.d.ts +23 -0
- package/dist/esm/lib/v3/verifier/prompts/render.js +41 -0
- package/dist/esm/lib/v3/verifier/prompts/render.js.map +1 -0
- package/dist/esm/lib/v3/verifier/prompts/rubricGeneration.d.ts +16 -0
- package/dist/esm/lib/v3/verifier/prompts/rubricGeneration.js +318 -0
- package/dist/esm/lib/v3/verifier/prompts/rubricGeneration.js.map +1 -0
- package/dist/esm/lib/v3/verifier/prompts/taskValidity.d.ts +1 -0
- package/dist/esm/lib/v3/verifier/prompts/taskValidity.js +113 -0
- package/dist/esm/lib/v3/verifier/prompts/taskValidity.js.map +1 -0
- package/dist/esm/lib/v3/verifier/rubricVerifier.d.ts +91 -0
- package/dist/esm/lib/v3/verifier/rubricVerifier.js +1640 -0
- package/dist/esm/lib/v3/verifier/rubricVerifier.js.map +1 -0
- package/dist/esm/lib/v3/verifier/trajectory.d.ts +3 -3
- package/dist/esm/lib/v3/verifier/trajectory.js +26 -26
- package/dist/esm/lib/v3/verifier/trajectory.js.map +1 -1
- package/dist/esm/lib/v3/verifier/types.d.ts +163 -15
- package/dist/esm/lib/v3/verifier/types.js +0 -7
- package/dist/esm/lib/v3/verifier/types.js.map +1 -1
- package/dist/esm/lib/v3Evaluator.d.ts +5 -0
- package/dist/esm/lib/v3Evaluator.js +47 -7
- package/dist/esm/lib/v3Evaluator.js.map +1 -1
- package/dist/esm/lib/version.d.ts +1 -1
- package/dist/esm/lib/version.js +1 -1
- package/dist/esm/lib/version.js.map +1 -1
- package/package.json +8 -8
- package/dist/cjs/lib/v3/dom/build/selectorRuntime.generated.d.ts +0 -24
- package/dist/cjs/lib/v3/dom/build/selectorRuntime.generated.js +0 -31
- package/dist/cjs/lib/v3/dom/build/selectorRuntime.generated.js.map +0 -1
- package/dist/esm/lib/v3/dom/build/selectorRuntime.generated.d.ts +0 -24
- package/dist/esm/lib/v3/dom/build/selectorRuntime.generated.js +0 -28
- package/dist/esm/lib/v3/dom/build/selectorRuntime.generated.js.map +0 -1
|
@@ -0,0 +1,1645 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RubricVerifier = void 0;
|
|
4
|
+
exports.resolveVerifierConfig = resolveVerifierConfig;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const trajectory_js_1 = require("./trajectory.js");
|
|
7
|
+
const index_js_1 = require("./prompts/index.js");
|
|
8
|
+
const evidence_js_1 = require("./evidence.js");
|
|
9
|
+
const errorTaxonomy_js_1 = require("./errorTaxonomy.js");
|
|
10
|
+
const RubricItemSchema = zod_1.z.object({
|
|
11
|
+
criterion: zod_1.z.string(),
|
|
12
|
+
description: zod_1.z.string(),
|
|
13
|
+
max_points: zod_1.z.number(),
|
|
14
|
+
condition: zod_1.z.string().optional(),
|
|
15
|
+
task_span: zod_1.z.string().optional(),
|
|
16
|
+
justification: zod_1.z.string().optional(),
|
|
17
|
+
earned_points: zod_1.z.union([zod_1.z.number(), zod_1.z.string()]).optional(),
|
|
18
|
+
});
|
|
19
|
+
const RubricSchema = zod_1.z.object({
|
|
20
|
+
items: zod_1.z.array(RubricItemSchema),
|
|
21
|
+
});
|
|
22
|
+
const FindingSchema = zod_1.z.object({
|
|
23
|
+
category: zod_1.z
|
|
24
|
+
.enum([
|
|
25
|
+
"agent_tool_usage",
|
|
26
|
+
"agent_strategy",
|
|
27
|
+
"rubric_quality",
|
|
28
|
+
"trajectory_capture",
|
|
29
|
+
"task_specification",
|
|
30
|
+
"verifier_uncertainty",
|
|
31
|
+
"other",
|
|
32
|
+
])
|
|
33
|
+
.catch("other"),
|
|
34
|
+
severity: zod_1.z.enum(["info", "warning", "blocking"]).catch("info"),
|
|
35
|
+
description: zod_1.z.string(),
|
|
36
|
+
suggestedAction: zod_1.z.string().optional(),
|
|
37
|
+
relatedSteps: zod_1.z.array(zod_1.z.number()).optional(),
|
|
38
|
+
});
|
|
39
|
+
const FusedOutcomeSchema = zod_1.z.object({
|
|
40
|
+
primary_intent: zod_1.z.string(),
|
|
41
|
+
reasoning: zod_1.z.string(),
|
|
42
|
+
output_success: zod_1.z.boolean(),
|
|
43
|
+
findings: zod_1.z.array(FindingSchema).optional().default([]),
|
|
44
|
+
});
|
|
45
|
+
const FusedPerCriterionSchema = zod_1.z.object({
|
|
46
|
+
criterion_idx: zod_1.z.coerce.number().int().min(0),
|
|
47
|
+
applicable_evidence: zod_1.z.string().optional().default(""),
|
|
48
|
+
justification: zod_1.z.string().optional().default(""),
|
|
49
|
+
earned_points: zod_1.z.coerce.number(),
|
|
50
|
+
evidence_sufficient: zod_1.z.boolean().optional().default(true),
|
|
51
|
+
condition_met: zod_1.z.boolean().nullable().optional(),
|
|
52
|
+
});
|
|
53
|
+
const FusedFailurePointSchema = zod_1.z.object({
|
|
54
|
+
step_index: zod_1.z.coerce.number().int(),
|
|
55
|
+
error_code: zod_1.z.string(),
|
|
56
|
+
error_category: zod_1.z.string(),
|
|
57
|
+
description: zod_1.z.string(),
|
|
58
|
+
});
|
|
59
|
+
const FusedTaskValiditySchema = zod_1.z.object({
|
|
60
|
+
is_ambiguous: zod_1.z.boolean(),
|
|
61
|
+
ambiguity_reason: zod_1.z.string().optional().default(""),
|
|
62
|
+
is_invalid: zod_1.z.boolean(),
|
|
63
|
+
invalid_reason: zod_1.z.string().optional().default(""),
|
|
64
|
+
});
|
|
65
|
+
const FusedJudgmentResponseSchema = zod_1.z.object({
|
|
66
|
+
outcome: FusedOutcomeSchema,
|
|
67
|
+
per_criterion: zod_1.z.array(FusedPerCriterionSchema),
|
|
68
|
+
failure_point: FusedFailurePointSchema.optional(),
|
|
69
|
+
task_validity: FusedTaskValiditySchema.optional(),
|
|
70
|
+
});
|
|
71
|
+
/** Outcome-only response: no per_criterion field, just outcome + diagnostics. */
|
|
72
|
+
const FusedOutcomeResponseSchema = zod_1.z.object({
|
|
73
|
+
outcome: FusedOutcomeSchema,
|
|
74
|
+
failure_point: FusedFailurePointSchema.optional(),
|
|
75
|
+
task_validity: FusedTaskValiditySchema.optional(),
|
|
76
|
+
});
|
|
77
|
+
const BatchedRelevanceItemSchema = zod_1.z.object({
|
|
78
|
+
evidence_idx: zod_1.z.coerce.number().int().min(0),
|
|
79
|
+
scores: zod_1.z.array(zod_1.z.object({
|
|
80
|
+
criterion_idx: zod_1.z.coerce.number().int().min(0),
|
|
81
|
+
score: zod_1.z.coerce.number().int().min(0).max(10),
|
|
82
|
+
})),
|
|
83
|
+
});
|
|
84
|
+
const BatchedRelevanceResponseSchema = zod_1.z.object({
|
|
85
|
+
items: zod_1.z.array(BatchedRelevanceItemSchema),
|
|
86
|
+
});
|
|
87
|
+
const PerCriterionScoreResponseSchema = zod_1.z.object({
|
|
88
|
+
criterion_idx: zod_1.z.coerce.number().int().min(0),
|
|
89
|
+
applicable_evidence: zod_1.z.string().optional().default(""),
|
|
90
|
+
justification: zod_1.z.string().optional().default(""),
|
|
91
|
+
earned_points: zod_1.z.coerce.number(),
|
|
92
|
+
evidence_sufficient: zod_1.z.boolean().optional().default(true),
|
|
93
|
+
condition_met: zod_1.z.boolean().nullable().optional(),
|
|
94
|
+
});
|
|
95
|
+
const TaskValiditySchema = zod_1.z.object({
|
|
96
|
+
reasoning_is_ambiguous: zod_1.z.string(),
|
|
97
|
+
is_ambiguous: zod_1.z.boolean(),
|
|
98
|
+
ambiguity_codes: zod_1.z.array(zod_1.z.string()).default([]),
|
|
99
|
+
reasoning_is_invalid: zod_1.z.string(),
|
|
100
|
+
is_invalid: zod_1.z.boolean(),
|
|
101
|
+
invalid_task_codes: zod_1.z.array(zod_1.z.string()).default([]),
|
|
102
|
+
});
|
|
103
|
+
const FailurePointSchema = zod_1.z.object({
|
|
104
|
+
step_numbers: zod_1.z.string(),
|
|
105
|
+
error_code: zod_1.z.string(),
|
|
106
|
+
error_category: zod_1.z.string(),
|
|
107
|
+
error_type: zod_1.z.string(),
|
|
108
|
+
what_happened: zod_1.z.string(),
|
|
109
|
+
agent_reasoning: zod_1.z.string(),
|
|
110
|
+
evidence: zod_1.z.string(),
|
|
111
|
+
impact: zod_1.z.string(),
|
|
112
|
+
});
|
|
113
|
+
const FailureAnalysisSchema = zod_1.z.object({
|
|
114
|
+
reasoning: zod_1.z.string(),
|
|
115
|
+
has_failure: zod_1.z.boolean(),
|
|
116
|
+
failure_points: zod_1.z.array(FailurePointSchema).default([]),
|
|
117
|
+
});
|
|
118
|
+
const noopLogger = () => { };
|
|
119
|
+
const APPROX_CHARS_PER_TOKEN = 4;
|
|
120
|
+
const DEFAULT_ACTION_HISTORY_TOKEN_BUDGET = 2_000;
|
|
121
|
+
const DEFAULT_EVIDENCE_TOKEN_BUDGET = 3_000;
|
|
122
|
+
const DEFAULT_OUTCOME_EVIDENCE_TOKEN_BUDGET = 4_000;
|
|
123
|
+
const DEFAULT_OUTCOME_IMAGE_LIMIT = 3;
|
|
124
|
+
const DEFAULT_MAX_PARALLEL = 8;
|
|
125
|
+
const DEFAULT_TOP_K = 5;
|
|
126
|
+
const DEFAULT_RELEVANCE_BATCH_SIZE = 4;
|
|
127
|
+
const OUTCOME_EVIDENCE_MAX_STEPS = 14;
|
|
128
|
+
const OUTCOME_EVIDENCE_STEP_CHARS = 900;
|
|
129
|
+
/**
|
|
130
|
+
* How much of the final aria tree to include in the always-attached
|
|
131
|
+
* "Final trajectory state" block. The verifier needs to see the end-of-run
|
|
132
|
+
* page content reliably — the top-K/relevance selection can starve it out
|
|
133
|
+
* when the final probe doesn't textually match the task keywords. 20k chars
|
|
134
|
+
* (~5k tokens) is comfortably above typical page sizes while bounded.
|
|
135
|
+
*/
|
|
136
|
+
const FINAL_STATE_ARIA_CHARS = 20_000;
|
|
137
|
+
const DEFAULT_APPROACH = "b";
|
|
138
|
+
const DEFAULT_OPTIONAL_STEPS_MODE = "folded";
|
|
139
|
+
const NO_TRUNC = Number.MAX_SAFE_INTEGER;
|
|
140
|
+
function readPositiveIntEnv(env, name, fallback) {
|
|
141
|
+
const raw = env[name];
|
|
142
|
+
if (!raw)
|
|
143
|
+
return fallback;
|
|
144
|
+
const parsed = Number.parseInt(raw, 10);
|
|
145
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
146
|
+
}
|
|
147
|
+
function readChars(env, name, fallback, disabled) {
|
|
148
|
+
if (disabled)
|
|
149
|
+
return NO_TRUNC;
|
|
150
|
+
return readPositiveIntEnv(env, name, fallback);
|
|
151
|
+
}
|
|
152
|
+
function readApproach(env) {
|
|
153
|
+
const raw = env.VERIFIER_APPROACH;
|
|
154
|
+
if (raw === "a" || raw === "b" || raw === "outcome-only")
|
|
155
|
+
return raw;
|
|
156
|
+
return DEFAULT_APPROACH;
|
|
157
|
+
}
|
|
158
|
+
function readOptionalsMode(env) {
|
|
159
|
+
const raw = env.VERIFIER_OPTIONAL_STEPS;
|
|
160
|
+
if (raw === "folded" || raw === "separate" || raw === "skip")
|
|
161
|
+
return raw;
|
|
162
|
+
return DEFAULT_OPTIONAL_STEPS_MODE;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Resolve every verifier knob from env (+ optional overrides) into a frozen
|
|
166
|
+
* VerifierConfig. Called once by RubricVerifier's constructor; per-call
|
|
167
|
+
* overrides flow through verify()'s optional override arg.
|
|
168
|
+
*
|
|
169
|
+
* The master switch VERIFIER_DISABLE_TRUNCATION=1 lifts every per-section
|
|
170
|
+
* limit to MAX_SAFE_INTEGER — useful on high-context models where
|
|
171
|
+
* evidence-bound truncation is the bottleneck, not the token budget.
|
|
172
|
+
*/
|
|
173
|
+
function resolveVerifierConfig(env = process.env, overrides = {}) {
|
|
174
|
+
const truncDisabled = overrides.truncation?.disabled ?? env.VERIFIER_DISABLE_TRUNCATION === "1";
|
|
175
|
+
return {
|
|
176
|
+
approach: overrides.approach ?? readApproach(env),
|
|
177
|
+
optionalSteps: overrides.optionalSteps ?? readOptionalsMode(env),
|
|
178
|
+
topK: overrides.topK ??
|
|
179
|
+
readPositiveIntEnv(env, "VERIFIER_TOP_K", DEFAULT_TOP_K),
|
|
180
|
+
relevanceBatchSize: overrides.relevanceBatchSize ??
|
|
181
|
+
readPositiveIntEnv(env, "VERIFIER_RELEVANCE_BATCH_SIZE", DEFAULT_RELEVANCE_BATCH_SIZE),
|
|
182
|
+
outcomeMaxImages: overrides.outcomeMaxImages ??
|
|
183
|
+
readPositiveIntEnv(env, "VERIFIER_OUTCOME_MAX_IMAGES", DEFAULT_OUTCOME_IMAGE_LIMIT),
|
|
184
|
+
maxParallel: overrides.maxParallel ??
|
|
185
|
+
readPositiveIntEnv(env, "VERIFIER_MAX_PARALLEL", DEFAULT_MAX_PARALLEL),
|
|
186
|
+
evidenceTokenBudget: overrides.evidenceTokenBudget ??
|
|
187
|
+
readPositiveIntEnv(env, "VERIFIER_EVIDENCE_TOKEN_BUDGET", DEFAULT_EVIDENCE_TOKEN_BUDGET),
|
|
188
|
+
outcomeEvidenceTokenBudget: overrides.outcomeEvidenceTokenBudget ??
|
|
189
|
+
readPositiveIntEnv(env, "VERIFIER_OUTCOME_EVIDENCE_TOKEN_BUDGET", DEFAULT_OUTCOME_EVIDENCE_TOKEN_BUDGET),
|
|
190
|
+
actionHistoryTokenBudget: overrides.actionHistoryTokenBudget ??
|
|
191
|
+
readPositiveIntEnv(env, "VERIFIER_ACTION_HISTORY_TOKEN_BUDGET", DEFAULT_ACTION_HISTORY_TOKEN_BUDGET),
|
|
192
|
+
truncation: {
|
|
193
|
+
disabled: truncDisabled,
|
|
194
|
+
evidenceTextPreview: overrides.truncation?.evidenceTextPreview ??
|
|
195
|
+
readChars(env, "VERIFIER_EVIDENCE_TEXT_PREVIEW_CHARS", 200, truncDisabled),
|
|
196
|
+
groupedEvidenceText: overrides.truncation?.groupedEvidenceText ??
|
|
197
|
+
readChars(env, "VERIFIER_GROUPED_EVIDENCE_TEXT_CHARS", 600, truncDisabled),
|
|
198
|
+
buildEvidenceText: overrides.truncation?.buildEvidenceText ??
|
|
199
|
+
readChars(env, "VERIFIER_BUILD_EVIDENCE_TEXT_CHARS", 160, truncDisabled),
|
|
200
|
+
buildEvidenceAria: overrides.truncation?.buildEvidenceAria ??
|
|
201
|
+
readChars(env, "VERIFIER_BUILD_EVIDENCE_ARIA_CHARS", 1200, truncDisabled),
|
|
202
|
+
actionHistoryReasoning: overrides.truncation?.actionHistoryReasoning ??
|
|
203
|
+
readChars(env, "VERIFIER_ACTION_HISTORY_REASONING_CHARS", 140, truncDisabled),
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function mergeConfig(base, overrides) {
|
|
208
|
+
if (!overrides)
|
|
209
|
+
return base;
|
|
210
|
+
return {
|
|
211
|
+
...base,
|
|
212
|
+
...overrides,
|
|
213
|
+
truncation: { ...base.truncation, ...(overrides.truncation ?? {}) },
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/** Top-K grouping per criterion. Pure compute. */
|
|
217
|
+
function groupTopKByCriterion(args) {
|
|
218
|
+
const { numCriteria, relevanceScores, topK } = args;
|
|
219
|
+
const grouped = new Map();
|
|
220
|
+
for (let cIdx = 0; cIdx < numCriteria; cIdx++) {
|
|
221
|
+
const scored = [];
|
|
222
|
+
for (const [eIdx, scoreMap] of relevanceScores.entries()) {
|
|
223
|
+
scored.push({ eIdx, score: scoreMap.get(cIdx) ?? 0 });
|
|
224
|
+
}
|
|
225
|
+
scored.sort((a, b) => {
|
|
226
|
+
if (b.score !== a.score)
|
|
227
|
+
return b.score - a.score;
|
|
228
|
+
return a.eIdx - b.eIdx; // ties → chronological order
|
|
229
|
+
});
|
|
230
|
+
const topKEvidence = scored.slice(0, topK);
|
|
231
|
+
// Relevance-floor filter: if any selected evidence scored ≥6,
|
|
232
|
+
// drop low-relevance entries that are >2 points below the weakest
|
|
233
|
+
// high-relevance entry.
|
|
234
|
+
const highScores = topKEvidence
|
|
235
|
+
.filter((s) => s.score >= 6)
|
|
236
|
+
.map((s) => s.score);
|
|
237
|
+
if (highScores.length === 0) {
|
|
238
|
+
grouped.set(cIdx, topKEvidence.map((s) => s.eIdx));
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const minHigh = Math.min(...highScores);
|
|
242
|
+
const kept = topKEvidence.filter((s) => !(s.score < 5 && minHigh - s.score > 2));
|
|
243
|
+
grouped.set(cIdx, (kept.length > 0 ? kept : topKEvidence).map((s) => s.eIdx));
|
|
244
|
+
}
|
|
245
|
+
return grouped;
|
|
246
|
+
}
|
|
247
|
+
function mapFusedPerCriterionToScores(rubric, perCriterion) {
|
|
248
|
+
const byIdx = new Map();
|
|
249
|
+
for (const entry of perCriterion)
|
|
250
|
+
byIdx.set(entry.criterion_idx, entry);
|
|
251
|
+
return rubric.items.map((c, i) => {
|
|
252
|
+
const entry = byIdx.get(i);
|
|
253
|
+
if (!entry) {
|
|
254
|
+
return {
|
|
255
|
+
criterion: c.criterion,
|
|
256
|
+
maxPoints: c.maxPoints,
|
|
257
|
+
earnedPoints: null,
|
|
258
|
+
explanation: "Verifier did not return a score for this criterion.",
|
|
259
|
+
evidenceInsufficient: true,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const clamped = Math.max(0, Math.min(c.maxPoints, entry.earned_points));
|
|
263
|
+
return {
|
|
264
|
+
criterion: c.criterion,
|
|
265
|
+
maxPoints: c.maxPoints,
|
|
266
|
+
earnedPoints: clamped,
|
|
267
|
+
explanation: entry.justification,
|
|
268
|
+
evidenceInsufficient: entry.evidence_sufficient === false,
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function evidencePreview(point, previewChars) {
|
|
273
|
+
if ((0, evidence_js_1.isImageEvidence)(point)) {
|
|
274
|
+
return `Screenshot at step ${point.originalStepIndex} (${point.bytes.length} bytes, ${point.mediaType})`;
|
|
275
|
+
}
|
|
276
|
+
const preview = point.content.slice(0, previewChars);
|
|
277
|
+
return `${textEvidenceLabel(point)} at step ${point.originalStepIndex} — "${preview.replace(/\s+/g, " ")}${point.content.length > previewChars ? "…" : ""}"`;
|
|
278
|
+
}
|
|
279
|
+
function textEvidenceLabel(point) {
|
|
280
|
+
switch (point.source) {
|
|
281
|
+
case "probe-aria":
|
|
282
|
+
return "ariaTree";
|
|
283
|
+
case "agent-text":
|
|
284
|
+
return "agent text";
|
|
285
|
+
case "agent-json":
|
|
286
|
+
return "agent JSON";
|
|
287
|
+
case "tool-output":
|
|
288
|
+
return "tool output";
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function renderEvidenceManifest(points, previewChars) {
|
|
292
|
+
if (points.length === 0)
|
|
293
|
+
return "(no evidence captured)";
|
|
294
|
+
return points
|
|
295
|
+
.map((p) => `- evidence_idx=${p.canonicalIndex}: ${evidencePreview(p, previewChars)}`)
|
|
296
|
+
.join("\n");
|
|
297
|
+
}
|
|
298
|
+
function renderGroupedEvidenceForApproach(rubric, evidence, groupedTopK, textLimit) {
|
|
299
|
+
if (evidence.length === 0)
|
|
300
|
+
return "(no evidence captured)";
|
|
301
|
+
const byIdx = new Map();
|
|
302
|
+
for (const e of evidence)
|
|
303
|
+
byIdx.set(e.canonicalIndex, e);
|
|
304
|
+
const sections = [];
|
|
305
|
+
for (let cIdx = 0; cIdx < rubric.items.length; cIdx++) {
|
|
306
|
+
const c = rubric.items[cIdx];
|
|
307
|
+
const topK = groupedTopK.get(cIdx) ?? [];
|
|
308
|
+
if (topK.length === 0) {
|
|
309
|
+
sections.push(`### Criterion ${cIdx}: ${c.criterion}\n(no evidence scored highly enough — rely on action history)`);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const body = topK
|
|
313
|
+
.map((eIdx) => {
|
|
314
|
+
const p = byIdx.get(eIdx);
|
|
315
|
+
if (!p)
|
|
316
|
+
return null;
|
|
317
|
+
if ((0, evidence_js_1.isImageEvidence)(p)) {
|
|
318
|
+
return `- Evidence #${eIdx} — image @ step=${p.originalStepIndex}`;
|
|
319
|
+
}
|
|
320
|
+
const text = p.content.replace(/\s+/g, " ").slice(0, textLimit);
|
|
321
|
+
return `- Evidence #${eIdx} — ${textEvidenceLabel(p)} @ step=${p.originalStepIndex}: "${text}${p.content.length > textLimit ? "…" : ""}"`;
|
|
322
|
+
})
|
|
323
|
+
.filter((x) => x !== null)
|
|
324
|
+
.join("\n");
|
|
325
|
+
sections.push(`### Criterion ${cIdx}: ${c.criterion}\n${body}`);
|
|
326
|
+
}
|
|
327
|
+
return sections.join("\n\n");
|
|
328
|
+
}
|
|
329
|
+
class RubricVerifier {
|
|
330
|
+
getClient;
|
|
331
|
+
getRubricGenClient;
|
|
332
|
+
logger;
|
|
333
|
+
baseConfig;
|
|
334
|
+
constructor(opts) {
|
|
335
|
+
this.getClient = opts.getClient;
|
|
336
|
+
this.getRubricGenClient = opts.getRubricGenClient ?? opts.getClient;
|
|
337
|
+
this.logger = opts.logger ?? noopLogger;
|
|
338
|
+
this.baseConfig = resolveVerifierConfig(process.env, opts.config);
|
|
339
|
+
}
|
|
340
|
+
/** Resolved verifier knobs the constructor saw, frozen at construction. */
|
|
341
|
+
get config() {
|
|
342
|
+
return this.baseConfig;
|
|
343
|
+
}
|
|
344
|
+
async verify(trajectory, overrides) {
|
|
345
|
+
const taskSpec = trajectory.task;
|
|
346
|
+
const config = mergeConfig(this.baseConfig, overrides);
|
|
347
|
+
const hasTrajectorySignal = trajectory.steps.length > 0 || Boolean(trajectory.finalAnswer?.trim());
|
|
348
|
+
if (!hasTrajectorySignal) {
|
|
349
|
+
return this.emptyTrajectoryResult((0, trajectory_js_1.normalizeRubric)(taskSpec.precomputedRubric));
|
|
350
|
+
}
|
|
351
|
+
const { approach, optionalSteps: optionalsMode } = config;
|
|
352
|
+
if (approach === "outcome-only") {
|
|
353
|
+
return this.verifyOutcomeOnly(trajectory, taskSpec, config);
|
|
354
|
+
}
|
|
355
|
+
let rubric = (0, trajectory_js_1.normalizeRubric)(taskSpec.precomputedRubric);
|
|
356
|
+
const rubricSource = rubric ? "precomputed" : "generated";
|
|
357
|
+
if (!rubric) {
|
|
358
|
+
rubric = await this.generateRubric(taskSpec);
|
|
359
|
+
}
|
|
360
|
+
// Empty-evidence trajectories fall back gracefully — the chosen approach
|
|
361
|
+
// degrades to an action-history-only judgment downstream.
|
|
362
|
+
const { evidence, loaded } = await (0, evidence_js_1.collectCanonicalEvidence)(trajectory);
|
|
363
|
+
const relevanceScores = await this.scoreRelevanceBatched({
|
|
364
|
+
taskSpec,
|
|
365
|
+
rubric,
|
|
366
|
+
evidence,
|
|
367
|
+
config,
|
|
368
|
+
});
|
|
369
|
+
const groupedTopK = groupTopKByCriterion({
|
|
370
|
+
numCriteria: rubric.items.length,
|
|
371
|
+
relevanceScores,
|
|
372
|
+
topK: config.topK,
|
|
373
|
+
});
|
|
374
|
+
let perCriterion;
|
|
375
|
+
let fusedOutcome;
|
|
376
|
+
let foldedFailurePoint;
|
|
377
|
+
let foldedTaskValidity;
|
|
378
|
+
if (approach === "b") {
|
|
379
|
+
const fused = await this.fusedJudgment({
|
|
380
|
+
trajectory,
|
|
381
|
+
taskSpec,
|
|
382
|
+
rubric,
|
|
383
|
+
evidence,
|
|
384
|
+
groupedTopK,
|
|
385
|
+
foldFailure: optionalsMode === "folded",
|
|
386
|
+
foldValidity: optionalsMode === "folded",
|
|
387
|
+
config,
|
|
388
|
+
});
|
|
389
|
+
perCriterion = mapFusedPerCriterionToScores(rubric, fused.per_criterion);
|
|
390
|
+
fusedOutcome = fused.outcome;
|
|
391
|
+
foldedFailurePoint = fused.failure_point;
|
|
392
|
+
foldedTaskValidity = fused.task_validity;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// Approach a: per-criterion analysis returns earned_points directly;
|
|
396
|
+
// no separate whole-rubric rescore.
|
|
397
|
+
perCriterion = await this.scorePerCriterion({
|
|
398
|
+
trajectory,
|
|
399
|
+
taskSpec,
|
|
400
|
+
rubric,
|
|
401
|
+
evidence,
|
|
402
|
+
groupedTopK,
|
|
403
|
+
config,
|
|
404
|
+
});
|
|
405
|
+
const outcome = await this.verifyOutcomeFused({
|
|
406
|
+
trajectory,
|
|
407
|
+
taskSpec,
|
|
408
|
+
rubric,
|
|
409
|
+
perCriterion,
|
|
410
|
+
evidence,
|
|
411
|
+
foldFailure: optionalsMode === "folded",
|
|
412
|
+
foldValidity: optionalsMode === "folded",
|
|
413
|
+
config,
|
|
414
|
+
});
|
|
415
|
+
fusedOutcome = outcome.outcome;
|
|
416
|
+
foldedFailurePoint = outcome.failure_point;
|
|
417
|
+
foldedTaskValidity = outcome.task_validity;
|
|
418
|
+
}
|
|
419
|
+
// ── Process score (deterministic from earned_points) ──────────────────
|
|
420
|
+
const totals = perCriterion.reduce((acc, c) => ({
|
|
421
|
+
earned: acc.earned + (c.earnedPoints ?? 0),
|
|
422
|
+
max: acc.max + c.maxPoints,
|
|
423
|
+
}), { earned: 0, max: 0 });
|
|
424
|
+
const processScore = totals.max > 0 ? totals.earned / totals.max : 0;
|
|
425
|
+
const evidenceInsufficient = perCriterion
|
|
426
|
+
.filter((c) => c.evidenceInsufficient)
|
|
427
|
+
.map((c) => c.criterion);
|
|
428
|
+
const findings = (fusedOutcome?.findings ?? []).map((f) => ({
|
|
429
|
+
...f,
|
|
430
|
+
category: f.category ?? "other",
|
|
431
|
+
severity: f.severity ?? "info",
|
|
432
|
+
}));
|
|
433
|
+
// ── Optional steps: folded, separate, or skipped ──────────────────────
|
|
434
|
+
let firstPointOfFailure;
|
|
435
|
+
if (foldedFailurePoint && !fusedOutcome?.output_success) {
|
|
436
|
+
firstPointOfFailure = {
|
|
437
|
+
stepIndex: foldedFailurePoint.step_index,
|
|
438
|
+
errorCode: foldedFailurePoint.error_code,
|
|
439
|
+
category: foldedFailurePoint.error_category,
|
|
440
|
+
description: foldedFailurePoint.description,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
else if (optionalsMode === "separate" &&
|
|
444
|
+
fusedOutcome &&
|
|
445
|
+
!fusedOutcome.output_success) {
|
|
446
|
+
firstPointOfFailure = await this.analyzeFailures({
|
|
447
|
+
trajectory,
|
|
448
|
+
taskSpec,
|
|
449
|
+
rubric,
|
|
450
|
+
perCriterion,
|
|
451
|
+
outcome: {
|
|
452
|
+
output_success: fusedOutcome.output_success,
|
|
453
|
+
primary_intent: fusedOutcome.primary_intent,
|
|
454
|
+
reasoning: fusedOutcome.reasoning,
|
|
455
|
+
findings: fusedOutcome.findings ?? [],
|
|
456
|
+
},
|
|
457
|
+
config,
|
|
458
|
+
}).catch(() => undefined);
|
|
459
|
+
}
|
|
460
|
+
let taskValidity;
|
|
461
|
+
if (foldedTaskValidity) {
|
|
462
|
+
taskValidity = {
|
|
463
|
+
isAmbiguous: foldedTaskValidity.is_ambiguous,
|
|
464
|
+
isInvalid: foldedTaskValidity.is_invalid,
|
|
465
|
+
ambiguityReason: foldedTaskValidity.is_ambiguous && foldedTaskValidity.ambiguity_reason
|
|
466
|
+
? foldedTaskValidity.ambiguity_reason
|
|
467
|
+
: undefined,
|
|
468
|
+
invalidReason: foldedTaskValidity.is_invalid && foldedTaskValidity.invalid_reason
|
|
469
|
+
? foldedTaskValidity.invalid_reason
|
|
470
|
+
: undefined,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
else if (optionalsMode === "separate") {
|
|
474
|
+
taskValidity = await this.classifyTaskValidity(taskSpec).catch(() => ({
|
|
475
|
+
isAmbiguous: false,
|
|
476
|
+
isInvalid: false,
|
|
477
|
+
}));
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
taskValidity = { isAmbiguous: false, isInvalid: false };
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
outcomeSuccess: fusedOutcome?.output_success ?? false,
|
|
484
|
+
processScore,
|
|
485
|
+
perCriterion,
|
|
486
|
+
taskValidity,
|
|
487
|
+
evidenceInsufficient,
|
|
488
|
+
findings: findings.length > 0 ? findings : undefined,
|
|
489
|
+
firstPointOfFailure,
|
|
490
|
+
rawSteps: {
|
|
491
|
+
primaryIntent: fusedOutcome?.primary_intent,
|
|
492
|
+
reasoning: fusedOutcome?.reasoning,
|
|
493
|
+
rubricSource,
|
|
494
|
+
approach,
|
|
495
|
+
optionalsMode,
|
|
496
|
+
totalEarned: totals.earned,
|
|
497
|
+
totalMax: totals.max,
|
|
498
|
+
evidenceImages: evidence.filter(evidence_js_1.isImageEvidence).length,
|
|
499
|
+
evidenceTexts: evidence.filter(evidence_js_1.isTextEvidence).length,
|
|
500
|
+
evidenceOriginalScreenshots: loaded.originalCount,
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
emptyTrajectoryResult(rubric) {
|
|
505
|
+
const items = rubric?.items ?? [];
|
|
506
|
+
return {
|
|
507
|
+
outcomeSuccess: false,
|
|
508
|
+
explanation: "No trajectory steps or final answer were captured; skipped verifier LLM calls.",
|
|
509
|
+
processScore: 0,
|
|
510
|
+
perCriterion: items.map((c) => ({
|
|
511
|
+
criterion: c.criterion,
|
|
512
|
+
maxPoints: c.maxPoints,
|
|
513
|
+
earnedPoints: 0,
|
|
514
|
+
explanation: "No trajectory steps or final answer were captured; skipped verifier LLM calls.",
|
|
515
|
+
evidenceInsufficient: true,
|
|
516
|
+
})),
|
|
517
|
+
taskValidity: { isAmbiguous: false, isInvalid: false },
|
|
518
|
+
evidenceInsufficient: items.map((c) => c.criterion),
|
|
519
|
+
rawSteps: {
|
|
520
|
+
reason: "empty-trajectory",
|
|
521
|
+
rubricSource: rubric ? "precomputed" : "none",
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
async verifyOutcomeOnly(trajectory, taskSpec, config) {
|
|
526
|
+
const foldFailure = config.optionalSteps === "folded";
|
|
527
|
+
const foldValidity = config.optionalSteps === "folded";
|
|
528
|
+
const taxonomyBlock = foldFailure
|
|
529
|
+
? `\n${(0, errorTaxonomy_js_1.getTaxonomyText)(1, 6, 4)}\n${(0, errorTaxonomy_js_1.getTaxonomyText)(7, 8, 4)}\n`
|
|
530
|
+
: "";
|
|
531
|
+
const prompt = (0, index_js_1.renderPrompt)(index_js_1.FUSED_OUTCOME_PROMPT, {
|
|
532
|
+
task_definition: taskSpec.instruction,
|
|
533
|
+
init_url_context: (0, index_js_1.buildInitUrlContext)(taskSpec.initUrl),
|
|
534
|
+
action_history: this.formatActionHistory(trajectory, config),
|
|
535
|
+
outcome_evidence_summary: this.buildOutcomeEvidenceSummary(trajectory, taskSpec, config),
|
|
536
|
+
final_state_block: buildFinalStateBlock(trajectory),
|
|
537
|
+
agent_predicted_output: trajectory.finalAnswer ?? "(no final answer recorded)",
|
|
538
|
+
rubric_summary: "(no rubric - outcome-only mode; judge success from the task, action history, final answer, and attached screenshots)",
|
|
539
|
+
taxonomy_block: taxonomyBlock,
|
|
540
|
+
fold_failure_analysis: foldFailure ? "true" : "false",
|
|
541
|
+
fold_task_validity: foldValidity ? "true" : "false",
|
|
542
|
+
current_date: currentDateForVerifier(),
|
|
543
|
+
});
|
|
544
|
+
const messageContent = [{ type: "text", text: prompt }];
|
|
545
|
+
// Final images first (always attached), then recent images deduped.
|
|
546
|
+
const seenImageKeys = new Set();
|
|
547
|
+
const attachImage = (img) => {
|
|
548
|
+
const key = `${img.bytes.length}:${img.bytes.subarray(0, 32).toString("base64")}`;
|
|
549
|
+
if (seenImageKeys.has(key))
|
|
550
|
+
return;
|
|
551
|
+
seenImageKeys.add(key);
|
|
552
|
+
messageContent.push({
|
|
553
|
+
type: "image_url",
|
|
554
|
+
image_url: {
|
|
555
|
+
url: `data:${img.mediaType};base64,${img.bytes.toString("base64")}`,
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
for (const img of selectFinalImages(trajectory))
|
|
560
|
+
attachImage(img);
|
|
561
|
+
for (const img of selectRecentImages(trajectory, config.outcomeMaxImages))
|
|
562
|
+
attachImage(img);
|
|
563
|
+
let fused;
|
|
564
|
+
try {
|
|
565
|
+
const client = this.getClient();
|
|
566
|
+
const response = await client.createChatCompletion({
|
|
567
|
+
logger: this.logger,
|
|
568
|
+
options: {
|
|
569
|
+
messages: [
|
|
570
|
+
{
|
|
571
|
+
role: "system",
|
|
572
|
+
content: "You are an expert evaluator of web-navigation agent trajectories. Output only valid JSON conforming to the schema in the user message.",
|
|
573
|
+
},
|
|
574
|
+
{ role: "user", content: messageContent },
|
|
575
|
+
],
|
|
576
|
+
response_model: {
|
|
577
|
+
name: "FusedOutcome",
|
|
578
|
+
schema: FusedOutcomeResponseSchema,
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
fused = response.data;
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
fused = {
|
|
586
|
+
outcome: {
|
|
587
|
+
primary_intent: taskSpec.instruction,
|
|
588
|
+
reasoning: "Outcome-only LLM call failed; defaulting to output_success=false.",
|
|
589
|
+
output_success: false,
|
|
590
|
+
findings: [
|
|
591
|
+
{
|
|
592
|
+
category: "verifier_uncertainty",
|
|
593
|
+
severity: "warning",
|
|
594
|
+
description: "The outcome-only verification call did not return a parseable response.",
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
const outcomeSuccess = fused.outcome.output_success;
|
|
601
|
+
const findings = (fused.outcome.findings ?? []).map((f) => ({
|
|
602
|
+
...f,
|
|
603
|
+
category: f.category ?? "other",
|
|
604
|
+
severity: f.severity ?? "info",
|
|
605
|
+
}));
|
|
606
|
+
let firstPointOfFailure;
|
|
607
|
+
if (fused.failure_point && !outcomeSuccess) {
|
|
608
|
+
firstPointOfFailure = {
|
|
609
|
+
stepIndex: fused.failure_point.step_index,
|
|
610
|
+
errorCode: fused.failure_point.error_code,
|
|
611
|
+
category: fused.failure_point.error_category,
|
|
612
|
+
description: fused.failure_point.description,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
const taskValidity = fused.task_validity
|
|
616
|
+
? {
|
|
617
|
+
isAmbiguous: fused.task_validity.is_ambiguous,
|
|
618
|
+
isInvalid: fused.task_validity.is_invalid,
|
|
619
|
+
ambiguityReason: fused.task_validity.is_ambiguous &&
|
|
620
|
+
fused.task_validity.ambiguity_reason
|
|
621
|
+
? fused.task_validity.ambiguity_reason
|
|
622
|
+
: undefined,
|
|
623
|
+
invalidReason: fused.task_validity.is_invalid && fused.task_validity.invalid_reason
|
|
624
|
+
? fused.task_validity.invalid_reason
|
|
625
|
+
: undefined,
|
|
626
|
+
}
|
|
627
|
+
: { isAmbiguous: false, isInvalid: false };
|
|
628
|
+
return {
|
|
629
|
+
outcomeSuccess,
|
|
630
|
+
explanation: fused.outcome.reasoning,
|
|
631
|
+
taskValidity,
|
|
632
|
+
findings: findings.length > 0 ? findings : undefined,
|
|
633
|
+
firstPointOfFailure,
|
|
634
|
+
rawSteps: {
|
|
635
|
+
primaryIntent: fused.outcome.primary_intent,
|
|
636
|
+
reasoning: fused.outcome.reasoning,
|
|
637
|
+
approach: "outcome-only",
|
|
638
|
+
optionalsMode: config.optionalSteps,
|
|
639
|
+
screenshotsAttached: seenImageKeys.size,
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Score every (evidence, criterion) pair with one batched call per chunk,
|
|
645
|
+
* to avoid a per-(criterion, frame) fan-out. Failed batches contribute
|
|
646
|
+
* all-zeros scores so the downstream top-K still produces valid groups.
|
|
647
|
+
*/
|
|
648
|
+
async scoreRelevanceBatched(args) {
|
|
649
|
+
const { taskSpec, rubric, evidence, config } = args;
|
|
650
|
+
const out = new Map();
|
|
651
|
+
if (evidence.length === 0)
|
|
652
|
+
return out;
|
|
653
|
+
const numCriteria = rubric.items.length;
|
|
654
|
+
const rubricCriteriaText = rubric.items
|
|
655
|
+
.map((c, i) => `\n${i}. **${c.criterion}**\n Description: ${c.description}\n`)
|
|
656
|
+
.join("");
|
|
657
|
+
const batchSize = Math.max(1, config.relevanceBatchSize);
|
|
658
|
+
const batches = [];
|
|
659
|
+
for (let i = 0; i < evidence.length; i += batchSize) {
|
|
660
|
+
batches.push(evidence.slice(i, i + batchSize));
|
|
661
|
+
}
|
|
662
|
+
const limit = pLimit(config.maxParallel);
|
|
663
|
+
const tasks = batches.map((batch) => limit(async () => {
|
|
664
|
+
const manifest = renderEvidenceManifest(batch, config.truncation.evidenceTextPreview);
|
|
665
|
+
const prompt = (0, index_js_1.renderPrompt)(index_js_1.MM_BATCHED_RELEVANCE_PROMPT, {
|
|
666
|
+
task_definition: taskSpec.instruction,
|
|
667
|
+
init_url_context: (0, index_js_1.buildInitUrlContext)(taskSpec.initUrl),
|
|
668
|
+
rubric_criteria: rubricCriteriaText,
|
|
669
|
+
evidence_manifest: manifest,
|
|
670
|
+
});
|
|
671
|
+
const messageContent = [{ type: "text", text: prompt }];
|
|
672
|
+
for (const ev of batch) {
|
|
673
|
+
if ((0, evidence_js_1.isImageEvidence)(ev)) {
|
|
674
|
+
messageContent.push({
|
|
675
|
+
type: "image_url",
|
|
676
|
+
image_url: {
|
|
677
|
+
url: `data:${ev.mediaType};base64,${ev.bytes.toString("base64")}`,
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
messageContent.push({
|
|
683
|
+
type: "text",
|
|
684
|
+
text: `\n[evidence_idx=${ev.canonicalIndex} — ${textEvidenceLabel(ev)} at step ${ev.originalStepIndex}]\n${ev.content}\n`,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
const client = this.getClient();
|
|
690
|
+
const response = await client.createChatCompletion({
|
|
691
|
+
logger: this.logger,
|
|
692
|
+
options: {
|
|
693
|
+
messages: [
|
|
694
|
+
{
|
|
695
|
+
role: "system",
|
|
696
|
+
content: "You are scoring how relevant each evidence point in a batch is to each rubric criterion. Output only valid JSON conforming to the schema in the user message.",
|
|
697
|
+
},
|
|
698
|
+
{ role: "user", content: messageContent },
|
|
699
|
+
],
|
|
700
|
+
response_model: {
|
|
701
|
+
name: "BatchedRelevance",
|
|
702
|
+
schema: BatchedRelevanceResponseSchema,
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
const data = response.data;
|
|
707
|
+
for (const item of data.items) {
|
|
708
|
+
const scoreMap = new Map();
|
|
709
|
+
for (const s of item.scores) {
|
|
710
|
+
if (s.criterion_idx >= 0 && s.criterion_idx < numCriteria) {
|
|
711
|
+
scoreMap.set(s.criterion_idx, s.score);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
for (let i = 0; i < numCriteria; i++) {
|
|
715
|
+
if (!scoreMap.has(i))
|
|
716
|
+
scoreMap.set(i, 0);
|
|
717
|
+
}
|
|
718
|
+
out.set(item.evidence_idx, scoreMap);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
// Per-batch failure: zero out the whole batch so the pipeline
|
|
723
|
+
// continues — top-K won't select these evidence points.
|
|
724
|
+
for (const ev of batch) {
|
|
725
|
+
const scoreMap = new Map();
|
|
726
|
+
for (let i = 0; i < numCriteria; i++)
|
|
727
|
+
scoreMap.set(i, 0);
|
|
728
|
+
out.set(ev.canonicalIndex, scoreMap);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}));
|
|
732
|
+
await Promise.all(tasks);
|
|
733
|
+
// Pad any missing evidence indices with zeros (defensive against the
|
|
734
|
+
// model omitting batch entries).
|
|
735
|
+
for (const ev of evidence) {
|
|
736
|
+
if (!out.has(ev.canonicalIndex)) {
|
|
737
|
+
const scoreMap = new Map();
|
|
738
|
+
for (let i = 0; i < numCriteria; i++)
|
|
739
|
+
scoreMap.set(i, 0);
|
|
740
|
+
out.set(ev.canonicalIndex, scoreMap);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return out;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* One call per rubric criterion. Each call sees the criterion's top-K
|
|
747
|
+
* evidence points (images + ariaTree snippets), the action history, and
|
|
748
|
+
* the final answer; the response includes `earned_points` directly so the
|
|
749
|
+
* process score is deterministic (Σ earned / Σ max).
|
|
750
|
+
*/
|
|
751
|
+
async scorePerCriterion(args) {
|
|
752
|
+
const { trajectory, taskSpec, rubric, evidence, groupedTopK, config } = args;
|
|
753
|
+
if (rubric.items.length === 0)
|
|
754
|
+
return [];
|
|
755
|
+
const evidenceByIdx = new Map();
|
|
756
|
+
for (const e of evidence)
|
|
757
|
+
evidenceByIdx.set(e.canonicalIndex, e);
|
|
758
|
+
const actionHistory = this.formatActionHistory(trajectory, config);
|
|
759
|
+
const predictedOutput = trajectory.finalAnswer ?? "(no final answer recorded)";
|
|
760
|
+
const limit = pLimit(config.maxParallel);
|
|
761
|
+
const tasks = rubric.items.map((criterion, cIdx) => limit(async () => {
|
|
762
|
+
const topK = groupedTopK.get(cIdx) ?? [];
|
|
763
|
+
const evidencePoints = topK
|
|
764
|
+
.map((eIdx) => evidenceByIdx.get(eIdx))
|
|
765
|
+
.filter((e) => e !== undefined);
|
|
766
|
+
const manifest = evidencePoints.length === 0
|
|
767
|
+
? "(no evidence scored highly enough for this criterion — rely on action history)"
|
|
768
|
+
: renderEvidenceManifest(evidencePoints, config.truncation.evidenceTextPreview);
|
|
769
|
+
const conditionLine = criterion.condition
|
|
770
|
+
? `- Condition: ${criterion.condition}`
|
|
771
|
+
: "";
|
|
772
|
+
const prompt = (0, index_js_1.renderPrompt)(index_js_1.MM_PER_CRITERION_SCORE_PROMPT, {
|
|
773
|
+
task_definition: taskSpec.instruction,
|
|
774
|
+
init_url_context: (0, index_js_1.buildInitUrlContext)(taskSpec.initUrl),
|
|
775
|
+
action_history: actionHistory,
|
|
776
|
+
agent_predicted_output: predictedOutput,
|
|
777
|
+
criterion_idx: cIdx,
|
|
778
|
+
criterion_name: criterion.criterion,
|
|
779
|
+
criterion_description: criterion.description,
|
|
780
|
+
criterion_max_points: criterion.maxPoints,
|
|
781
|
+
criterion_condition: conditionLine,
|
|
782
|
+
evidence_manifest: manifest,
|
|
783
|
+
});
|
|
784
|
+
const messageContent = [{ type: "text", text: prompt }];
|
|
785
|
+
for (const ev of evidencePoints) {
|
|
786
|
+
if ((0, evidence_js_1.isImageEvidence)(ev)) {
|
|
787
|
+
messageContent.push({
|
|
788
|
+
type: "image_url",
|
|
789
|
+
image_url: {
|
|
790
|
+
url: `data:${ev.mediaType};base64,${ev.bytes.toString("base64")}`,
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
messageContent.push({
|
|
796
|
+
type: "text",
|
|
797
|
+
text: `\n[evidence_idx=${ev.canonicalIndex} — ${textEvidenceLabel(ev)} at step ${ev.originalStepIndex}]\n${ev.content}\n`,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
const client = this.getClient();
|
|
803
|
+
const response = await client.createChatCompletion({
|
|
804
|
+
logger: this.logger,
|
|
805
|
+
options: {
|
|
806
|
+
messages: [
|
|
807
|
+
{
|
|
808
|
+
role: "system",
|
|
809
|
+
content: "You are scoring one rubric criterion against the most relevant evidence from an agent's trajectory. Output only valid JSON conforming to the schema in the user message.",
|
|
810
|
+
},
|
|
811
|
+
{ role: "user", content: messageContent },
|
|
812
|
+
],
|
|
813
|
+
response_model: {
|
|
814
|
+
name: "PerCriterionScore",
|
|
815
|
+
schema: PerCriterionScoreResponseSchema,
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
const data = response.data;
|
|
820
|
+
const clamped = Math.max(0, Math.min(criterion.maxPoints, data.earned_points));
|
|
821
|
+
return {
|
|
822
|
+
criterion: criterion.criterion,
|
|
823
|
+
maxPoints: criterion.maxPoints,
|
|
824
|
+
earnedPoints: clamped,
|
|
825
|
+
explanation: data.justification,
|
|
826
|
+
evidenceInsufficient: data.evidence_sufficient === false,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
return {
|
|
831
|
+
criterion: criterion.criterion,
|
|
832
|
+
maxPoints: criterion.maxPoints,
|
|
833
|
+
earnedPoints: null,
|
|
834
|
+
explanation: "Per-criterion scoring call failed; falling back to evidence-insufficient.",
|
|
835
|
+
evidenceInsufficient: true,
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
}));
|
|
839
|
+
return Promise.all(tasks);
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Single fused multimodal call returning the full EvaluationResult shape:
|
|
843
|
+
* rubric + per-criterion top-K evidence + action history + final answer.
|
|
844
|
+
* Optionally folds in first-point-of-failure and task-validity. Image
|
|
845
|
+
* evidence rides inline; ariaTree text is embedded in the prompt under
|
|
846
|
+
* each criterion's manifest section.
|
|
847
|
+
*/
|
|
848
|
+
async fusedJudgment(args) {
|
|
849
|
+
const { trajectory, taskSpec, rubric, evidence, groupedTopK, foldFailure, foldValidity, config, } = args;
|
|
850
|
+
const evidenceByIdx = new Map();
|
|
851
|
+
for (const e of evidence)
|
|
852
|
+
evidenceByIdx.set(e.canonicalIndex, e);
|
|
853
|
+
const usedImageIndices = new Set();
|
|
854
|
+
for (const topK of groupedTopK.values()) {
|
|
855
|
+
for (const eIdx of topK) {
|
|
856
|
+
const p = evidenceByIdx.get(eIdx);
|
|
857
|
+
if (p && (0, evidence_js_1.isImageEvidence)(p))
|
|
858
|
+
usedImageIndices.add(eIdx);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const usedImages = [...usedImageIndices]
|
|
862
|
+
.sort((a, b) => a - b)
|
|
863
|
+
.map((eIdx) => evidenceByIdx.get(eIdx))
|
|
864
|
+
.filter((p) => !!p && (0, evidence_js_1.isImageEvidence)(p));
|
|
865
|
+
const rubricBlock = rubric.items
|
|
866
|
+
.map((c, i) => {
|
|
867
|
+
const cond = c.condition ? `\n Condition: ${c.condition}` : "";
|
|
868
|
+
return `Criterion ${i} — "${c.criterion}" (max ${c.maxPoints} pts):\n Description: ${c.description}${cond}`;
|
|
869
|
+
})
|
|
870
|
+
.join("\n\n");
|
|
871
|
+
const evidenceBlock = renderGroupedEvidenceForApproach(rubric, evidence, groupedTopK, config.truncation.groupedEvidenceText);
|
|
872
|
+
const taxonomyBlock = foldFailure
|
|
873
|
+
? `\n${(0, errorTaxonomy_js_1.getTaxonomyText)(1, 6, 4)}\n${(0, errorTaxonomy_js_1.getTaxonomyText)(7, 8, 4)}\n`
|
|
874
|
+
: "";
|
|
875
|
+
const prompt = (0, index_js_1.renderPrompt)(index_js_1.FUSED_JUDGMENT_PROMPT, {
|
|
876
|
+
task_definition: taskSpec.instruction,
|
|
877
|
+
init_url_context: (0, index_js_1.buildInitUrlContext)(taskSpec.initUrl),
|
|
878
|
+
action_history: this.formatActionHistory(trajectory, config),
|
|
879
|
+
agent_predicted_output: trajectory.finalAnswer ?? "(no final answer recorded)",
|
|
880
|
+
rubric_block: rubricBlock,
|
|
881
|
+
evidence_block: evidenceBlock,
|
|
882
|
+
final_state_block: buildFinalStateBlock(trajectory),
|
|
883
|
+
taxonomy_block: taxonomyBlock,
|
|
884
|
+
fold_failure_analysis: foldFailure ? "true" : "false",
|
|
885
|
+
fold_task_validity: foldValidity ? "true" : "false",
|
|
886
|
+
current_date: currentDateForVerifier(),
|
|
887
|
+
});
|
|
888
|
+
const messageContent = [{ type: "text", text: prompt }];
|
|
889
|
+
// Always include the terminal visual evidence first — these bytes are the
|
|
890
|
+
// single most reliable signal of what the agent actually saw at the end
|
|
891
|
+
// of the run, and they're not subject to the per-criterion top-K cutoff.
|
|
892
|
+
// Dedupe by content so we don't pay tokens twice when the top-K already
|
|
893
|
+
// picked the same frame.
|
|
894
|
+
const finalImageBytes = new Set();
|
|
895
|
+
for (const img of selectFinalImages(trajectory)) {
|
|
896
|
+
const key = `${img.bytes.length}:${img.bytes.subarray(0, 32).toString("base64")}`;
|
|
897
|
+
finalImageBytes.add(key);
|
|
898
|
+
messageContent.push({
|
|
899
|
+
type: "image_url",
|
|
900
|
+
image_url: {
|
|
901
|
+
url: `data:${img.mediaType};base64,${img.bytes.toString("base64")}`,
|
|
902
|
+
},
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
for (const img of usedImages) {
|
|
906
|
+
const key = `${img.bytes.length}:${img.bytes.subarray(0, 32).toString("base64")}`;
|
|
907
|
+
if (finalImageBytes.has(key))
|
|
908
|
+
continue;
|
|
909
|
+
messageContent.push({
|
|
910
|
+
type: "image_url",
|
|
911
|
+
image_url: {
|
|
912
|
+
url: `data:${img.mediaType};base64,${img.bytes.toString("base64")}`,
|
|
913
|
+
},
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
const client = this.getClient();
|
|
918
|
+
const response = await client.createChatCompletion({
|
|
919
|
+
logger: this.logger,
|
|
920
|
+
options: {
|
|
921
|
+
messages: [
|
|
922
|
+
{
|
|
923
|
+
role: "system",
|
|
924
|
+
content: "You are an expert evaluator of web-navigation agent trajectories. Output only valid JSON conforming to the schema in the user message.",
|
|
925
|
+
},
|
|
926
|
+
{ role: "user", content: messageContent },
|
|
927
|
+
],
|
|
928
|
+
response_model: {
|
|
929
|
+
name: "FusedJudgment",
|
|
930
|
+
schema: FusedJudgmentResponseSchema,
|
|
931
|
+
},
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
return response.data;
|
|
935
|
+
}
|
|
936
|
+
catch (e) {
|
|
937
|
+
// Hard failure of the fused call: synthesize a no-confidence result
|
|
938
|
+
// so the pipeline can still produce an EvaluationResult object.
|
|
939
|
+
void e;
|
|
940
|
+
return {
|
|
941
|
+
outcome: {
|
|
942
|
+
primary_intent: taskSpec.instruction,
|
|
943
|
+
reasoning: "Fused judgment LLM call failed; returning evidence-insufficient result.",
|
|
944
|
+
output_success: false,
|
|
945
|
+
findings: [
|
|
946
|
+
{
|
|
947
|
+
category: "verifier_uncertainty",
|
|
948
|
+
severity: "warning",
|
|
949
|
+
description: "The fused judgment call did not return a parseable response.",
|
|
950
|
+
},
|
|
951
|
+
],
|
|
952
|
+
},
|
|
953
|
+
per_criterion: rubric.items.map((c, i) => ({
|
|
954
|
+
criterion_idx: i,
|
|
955
|
+
applicable_evidence: "",
|
|
956
|
+
justification: "Fused judgment call failed for this criterion.",
|
|
957
|
+
earned_points: 0,
|
|
958
|
+
evidence_sufficient: false,
|
|
959
|
+
})),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Consume the pre-scored rubric from scorePerCriterion and produce the
|
|
965
|
+
* outcome result. When foldFailure/foldValidity are set, the response also
|
|
966
|
+
* includes first-point-of-failure and task-validity, saving 1–2 extra
|
|
967
|
+
* LLM calls.
|
|
968
|
+
*/
|
|
969
|
+
async verifyOutcomeFused(args) {
|
|
970
|
+
const { trajectory, taskSpec, rubric, perCriterion, foldFailure, foldValidity, config, } = args;
|
|
971
|
+
void args.evidence;
|
|
972
|
+
const taxonomyBlock = foldFailure
|
|
973
|
+
? `\n${(0, errorTaxonomy_js_1.getTaxonomyText)(1, 6, 4)}\n${(0, errorTaxonomy_js_1.getTaxonomyText)(7, 8, 4)}\n`
|
|
974
|
+
: "";
|
|
975
|
+
const prompt = (0, index_js_1.renderPrompt)(index_js_1.FUSED_OUTCOME_PROMPT, {
|
|
976
|
+
task_definition: taskSpec.instruction,
|
|
977
|
+
init_url_context: (0, index_js_1.buildInitUrlContext)(taskSpec.initUrl),
|
|
978
|
+
action_history: this.formatActionHistory(trajectory, config),
|
|
979
|
+
outcome_evidence_summary: this.buildOutcomeEvidenceSummary(trajectory, taskSpec, config),
|
|
980
|
+
final_state_block: buildFinalStateBlock(trajectory),
|
|
981
|
+
agent_predicted_output: trajectory.finalAnswer ?? "(no final answer recorded)",
|
|
982
|
+
rubric_summary: this.formatScoredRubricSummary(rubric, perCriterion),
|
|
983
|
+
taxonomy_block: taxonomyBlock,
|
|
984
|
+
fold_failure_analysis: foldFailure ? "true" : "false",
|
|
985
|
+
fold_task_validity: foldValidity ? "true" : "false",
|
|
986
|
+
current_date: currentDateForVerifier(),
|
|
987
|
+
});
|
|
988
|
+
// Always attach the terminal screenshots so the outcome judge can see the
|
|
989
|
+
// final page state, regardless of the (formerly text-only) evidence
|
|
990
|
+
// summary heuristics.
|
|
991
|
+
const messageContent = [{ type: "text", text: prompt }];
|
|
992
|
+
for (const img of selectFinalImages(trajectory)) {
|
|
993
|
+
messageContent.push({
|
|
994
|
+
type: "image_url",
|
|
995
|
+
image_url: {
|
|
996
|
+
url: `data:${img.mediaType};base64,${img.bytes.toString("base64")}`,
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
const client = this.getClient();
|
|
1002
|
+
const response = await client.createChatCompletion({
|
|
1003
|
+
logger: this.logger,
|
|
1004
|
+
options: {
|
|
1005
|
+
messages: [
|
|
1006
|
+
{
|
|
1007
|
+
role: "system",
|
|
1008
|
+
content: "You are an expert evaluator of web-navigation agent trajectories. Output only valid JSON conforming to the schema in the user message.",
|
|
1009
|
+
},
|
|
1010
|
+
{ role: "user", content: messageContent },
|
|
1011
|
+
],
|
|
1012
|
+
response_model: {
|
|
1013
|
+
name: "FusedOutcome",
|
|
1014
|
+
schema: FusedOutcomeResponseSchema,
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
});
|
|
1018
|
+
return response.data;
|
|
1019
|
+
}
|
|
1020
|
+
catch {
|
|
1021
|
+
// Failure surfaces as a no-confidence result.
|
|
1022
|
+
return {
|
|
1023
|
+
outcome: {
|
|
1024
|
+
primary_intent: taskSpec.instruction,
|
|
1025
|
+
reasoning: "Outcome LLM call failed; defaulting to output_success=false.",
|
|
1026
|
+
output_success: false,
|
|
1027
|
+
findings: [
|
|
1028
|
+
{
|
|
1029
|
+
category: "verifier_uncertainty",
|
|
1030
|
+
severity: "warning",
|
|
1031
|
+
description: "The outcome verification call did not return a parseable response.",
|
|
1032
|
+
},
|
|
1033
|
+
],
|
|
1034
|
+
},
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Flat per-step evidence summary — fallback for trajectories with no
|
|
1040
|
+
* probe screenshots, such as harness-adapter or stubbed trajectories.
|
|
1041
|
+
*/
|
|
1042
|
+
buildEvidenceContext(trajectory, config, opts = {}) {
|
|
1043
|
+
if (trajectory.steps.length === 0) {
|
|
1044
|
+
return { text: "(no steps captured)", images: [] };
|
|
1045
|
+
}
|
|
1046
|
+
const textLimit = config.truncation.buildEvidenceText;
|
|
1047
|
+
const ariaLimit = config.truncation.buildEvidenceAria;
|
|
1048
|
+
const text = clampToTokenBudget(trajectory.steps
|
|
1049
|
+
.map((s, i) => {
|
|
1050
|
+
const url = s.probeEvidence.url ? `, url=${s.probeEvidence.url}` : "";
|
|
1051
|
+
const hasScreenshot = s.probeEvidence.screenshotPath || s.probeEvidence.screenshot
|
|
1052
|
+
? "yes"
|
|
1053
|
+
: "no";
|
|
1054
|
+
const tier1 = s.agentEvidence.modalities
|
|
1055
|
+
.map((m) => {
|
|
1056
|
+
if (m.type === "text")
|
|
1057
|
+
return `text(${m.content.slice(0, textLimit)})`;
|
|
1058
|
+
if (m.type === "image")
|
|
1059
|
+
return `image(${m.bytes.length} bytes)`;
|
|
1060
|
+
return `json(${safeJsonSnippet(m.content, 180)})`;
|
|
1061
|
+
})
|
|
1062
|
+
.join(", ");
|
|
1063
|
+
const toolOutput = safeJsonSnippet(s.toolOutput.result, 220);
|
|
1064
|
+
// Include the post-step a11y dump when captured — textual ground
|
|
1065
|
+
// truth for criteria that can't be verified from the visual probe
|
|
1066
|
+
// alone (prices, names, list contents). Per-step cap keeps the
|
|
1067
|
+
// total budget bounded.
|
|
1068
|
+
const ariaSnippet = typeof s.probeEvidence.ariaTree === "string" &&
|
|
1069
|
+
s.probeEvidence.ariaTree.length > 0
|
|
1070
|
+
? `\n aria_tree: ${s.probeEvidence.ariaTree.slice(0, ariaLimit)}${s.probeEvidence.ariaTree.length > ariaLimit
|
|
1071
|
+
? "… [truncated]"
|
|
1072
|
+
: ""}`
|
|
1073
|
+
: "";
|
|
1074
|
+
return `Screenshot ${i + 1} — step=${i}, action=${s.actionName}${url}, probe_screenshot=${hasScreenshot}\n tier1: ${tier1 || "(none)"}\n tool_output: ${toolOutput}${ariaSnippet}`;
|
|
1075
|
+
})
|
|
1076
|
+
.join("\n\n"), config.evidenceTokenBudget);
|
|
1077
|
+
if (opts.includeImages === false)
|
|
1078
|
+
return { text, images: [] };
|
|
1079
|
+
return {
|
|
1080
|
+
text,
|
|
1081
|
+
images: selectRecentImages(trajectory, config.outcomeMaxImages),
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Compact text evidence for the one-call outcome verifier.
|
|
1086
|
+
*
|
|
1087
|
+
* Outcome-only does not run the rubric relevance selector, but it still needs
|
|
1088
|
+
* enough saved-page text to avoid replacing trajectory facts with model
|
|
1089
|
+
* memory. Select a bounded set of lexically relevant and recent steps, then
|
|
1090
|
+
* include short excerpts around task/final-answer terms.
|
|
1091
|
+
*/
|
|
1092
|
+
buildOutcomeEvidenceSummary(trajectory, taskSpec, config) {
|
|
1093
|
+
if (trajectory.steps.length === 0)
|
|
1094
|
+
return "(no steps captured)";
|
|
1095
|
+
const keywords = outcomeKeywords(`${taskSpec.instruction}\n${trajectory.finalAnswer ?? ""}`);
|
|
1096
|
+
const lastImportantIndex = Math.max(0, trajectory.steps.length - 5);
|
|
1097
|
+
const candidates = trajectory.steps.map((step, position) => {
|
|
1098
|
+
const url = step.probeEvidence.url ?? "";
|
|
1099
|
+
const ariaTree = step.probeEvidence.ariaTree ?? "";
|
|
1100
|
+
const toolOutput = safeJsonSnippet(step.toolOutput?.result, 600);
|
|
1101
|
+
const actionArgs = safeJsonSnippet(step.actionArgs, 400);
|
|
1102
|
+
const haystack = [
|
|
1103
|
+
step.actionName,
|
|
1104
|
+
step.reasoning ?? "",
|
|
1105
|
+
url,
|
|
1106
|
+
actionArgs,
|
|
1107
|
+
toolOutput,
|
|
1108
|
+
ariaTree,
|
|
1109
|
+
]
|
|
1110
|
+
.join("\n")
|
|
1111
|
+
.toLowerCase();
|
|
1112
|
+
let score = position >= lastImportantIndex ? 3 : 0;
|
|
1113
|
+
if (url)
|
|
1114
|
+
score += 1;
|
|
1115
|
+
if (/extract|observe|aria|navigate|click|type|search/i.test(step.actionName)) {
|
|
1116
|
+
score += 1;
|
|
1117
|
+
}
|
|
1118
|
+
for (const keyword of keywords) {
|
|
1119
|
+
if (haystack.includes(keyword)) {
|
|
1120
|
+
score += keyword.length >= 8 ? 3 : 1;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return { step, position, score };
|
|
1124
|
+
});
|
|
1125
|
+
const selected = new Set();
|
|
1126
|
+
for (const candidate of [...candidates]
|
|
1127
|
+
.sort((a, b) => b.score - a.score || a.position - b.position)
|
|
1128
|
+
.slice(0, OUTCOME_EVIDENCE_MAX_STEPS)) {
|
|
1129
|
+
selected.add(candidate.position);
|
|
1130
|
+
}
|
|
1131
|
+
for (let i = Math.max(0, trajectory.steps.length - 4); i < trajectory.steps.length; i++) {
|
|
1132
|
+
selected.add(i);
|
|
1133
|
+
}
|
|
1134
|
+
const sections = [...selected]
|
|
1135
|
+
.sort((a, b) => a - b)
|
|
1136
|
+
.map((position) => {
|
|
1137
|
+
const step = trajectory.steps[position];
|
|
1138
|
+
const url = step.probeEvidence.url
|
|
1139
|
+
? ` url=${step.probeEvidence.url}`
|
|
1140
|
+
: "";
|
|
1141
|
+
const reasoning = step.reasoning
|
|
1142
|
+
? `\n reasoning: ${step.reasoning.slice(0, 220)}`
|
|
1143
|
+
: "";
|
|
1144
|
+
const toolOutput = step.toolOutput?.result
|
|
1145
|
+
? `\n tool_output: ${safeJsonSnippet(step.toolOutput.result, 320)}`
|
|
1146
|
+
: "";
|
|
1147
|
+
const ariaExcerpt = step.probeEvidence.ariaTree
|
|
1148
|
+
? `\n page_excerpt: ${bestOutcomeExcerpt(step.probeEvidence.ariaTree, keywords, OUTCOME_EVIDENCE_STEP_CHARS)}`
|
|
1149
|
+
: "";
|
|
1150
|
+
return `Step ${position}: ${step.actionName}(${summarizeArgs(step.actionArgs)})${url}${reasoning}${toolOutput}${ariaExcerpt}`;
|
|
1151
|
+
});
|
|
1152
|
+
return clampToTokenBudget(sections.join("\n\n"), config.outcomeEvidenceTokenBudget);
|
|
1153
|
+
}
|
|
1154
|
+
/** Generate a rubric from the task description alone. */
|
|
1155
|
+
async generateRubric(taskSpec) {
|
|
1156
|
+
const prompt = (0, index_js_1.renderPrompt)(index_js_1.RUBRIC_GENERATION_PROMPT, {
|
|
1157
|
+
task_id: taskSpec.instruction,
|
|
1158
|
+
init_url_context: (0, index_js_1.buildInitUrlContext)(taskSpec.initUrl),
|
|
1159
|
+
});
|
|
1160
|
+
const maxAttempts = 3;
|
|
1161
|
+
let lastError;
|
|
1162
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1163
|
+
try {
|
|
1164
|
+
const client = this.getRubricGenClient();
|
|
1165
|
+
const response = await client.createChatCompletion({
|
|
1166
|
+
logger: this.logger,
|
|
1167
|
+
options: {
|
|
1168
|
+
messages: [
|
|
1169
|
+
{
|
|
1170
|
+
role: "system",
|
|
1171
|
+
content: "You are an expert rubric author. Output only valid JSON conforming to the schema requested in the user message. Do not include explanatory prose.",
|
|
1172
|
+
},
|
|
1173
|
+
{ role: "user", content: prompt },
|
|
1174
|
+
],
|
|
1175
|
+
response_model: { name: "Rubric", schema: RubricSchema },
|
|
1176
|
+
},
|
|
1177
|
+
});
|
|
1178
|
+
const data = response.data;
|
|
1179
|
+
const normalized = (0, trajectory_js_1.normalizeRubric)({
|
|
1180
|
+
items: filterByTaskSpan(data.items, taskSpec.instruction, this.logger),
|
|
1181
|
+
});
|
|
1182
|
+
if (!normalized) {
|
|
1183
|
+
throw new Error("Rubric generation returned no rubric");
|
|
1184
|
+
}
|
|
1185
|
+
return normalized;
|
|
1186
|
+
}
|
|
1187
|
+
catch (err) {
|
|
1188
|
+
lastError = err;
|
|
1189
|
+
if (attempt === maxAttempts - 1)
|
|
1190
|
+
break;
|
|
1191
|
+
await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Identify all distinct failure points using taxonomy categories 1–6
|
|
1198
|
+
* (agent-controllable errors) and return the earliest one. Best-effort:
|
|
1199
|
+
* returns undefined on LLM failure / unparseable output / no failures
|
|
1200
|
+
* found, rather than blocking the rest of the pipeline.
|
|
1201
|
+
*/
|
|
1202
|
+
async analyzeFailures(args) {
|
|
1203
|
+
const { trajectory, taskSpec, rubric, perCriterion, outcome, config } = args;
|
|
1204
|
+
const evidenceContext = this.buildEvidenceContext(trajectory, config, {
|
|
1205
|
+
includeImages: false,
|
|
1206
|
+
});
|
|
1207
|
+
const prompt = (0, index_js_1.renderPrompt)(index_js_1.FIRST_POINT_OF_FAILURE_PROMPT, {
|
|
1208
|
+
task_definition: taskSpec.instruction,
|
|
1209
|
+
init_url_context: (0, index_js_1.buildInitUrlContext)(taskSpec.initUrl),
|
|
1210
|
+
action_history: this.formatActionHistory(trajectory, config),
|
|
1211
|
+
predicted_output: trajectory.finalAnswer ?? "(no final answer recorded)",
|
|
1212
|
+
rubric_summary: this.formatScoredRubricSummary(rubric, perCriterion),
|
|
1213
|
+
evidence_summary: evidenceContext.text,
|
|
1214
|
+
outcome_verification: `output_success=${outcome.output_success}\nprimary_intent=${outcome.primary_intent}\nreasoning=${outcome.reasoning}`,
|
|
1215
|
+
});
|
|
1216
|
+
const client = this.getClient();
|
|
1217
|
+
const response = await client.createChatCompletion({
|
|
1218
|
+
logger: this.logger,
|
|
1219
|
+
options: {
|
|
1220
|
+
messages: [
|
|
1221
|
+
{
|
|
1222
|
+
role: "system",
|
|
1223
|
+
content: "You are an expert failure analyst for computer-use web agents. Output only valid JSON conforming to the schema in the user message.",
|
|
1224
|
+
},
|
|
1225
|
+
{ role: "user", content: prompt },
|
|
1226
|
+
],
|
|
1227
|
+
response_model: {
|
|
1228
|
+
name: "FailureAnalysis",
|
|
1229
|
+
schema: FailureAnalysisSchema,
|
|
1230
|
+
},
|
|
1231
|
+
},
|
|
1232
|
+
});
|
|
1233
|
+
const data = response.data;
|
|
1234
|
+
if (!data.has_failure || data.failure_points.length === 0)
|
|
1235
|
+
return undefined;
|
|
1236
|
+
// Find the failure with the earliest step number: lowest min-step across
|
|
1237
|
+
// all failure_points.
|
|
1238
|
+
let best = null;
|
|
1239
|
+
for (const fp of data.failure_points) {
|
|
1240
|
+
const steps = (0, index_js_1.parseFailureStepNumbers)(fp.step_numbers, {
|
|
1241
|
+
maxStep: Math.max(0, trajectory.steps.length),
|
|
1242
|
+
});
|
|
1243
|
+
if (steps.length === 0)
|
|
1244
|
+
continue;
|
|
1245
|
+
const minStep = steps[0];
|
|
1246
|
+
if (best === null || minStep < best.minStep) {
|
|
1247
|
+
best = { minStep, point: fp };
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
if (best === null)
|
|
1251
|
+
return undefined;
|
|
1252
|
+
return {
|
|
1253
|
+
stepIndex: best.minStep,
|
|
1254
|
+
errorCode: best.point.error_code,
|
|
1255
|
+
category: best.point.error_category,
|
|
1256
|
+
description: `${best.point.error_type}: ${best.point.what_happened}`,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Classify the task across ambiguity (taxonomy category 7) and
|
|
1261
|
+
* validity/feasibility (category 8). Pure task-level analysis; no
|
|
1262
|
+
* trajectory context needed. Best-effort: returns undefined on LLM error.
|
|
1263
|
+
*/
|
|
1264
|
+
async classifyTaskValidity(taskSpec) {
|
|
1265
|
+
const prompt = (0, index_js_1.renderPrompt)(index_js_1.TASK_VALIDITY_PROMPT, {
|
|
1266
|
+
task_definition: taskSpec.instruction,
|
|
1267
|
+
url: taskSpec.initUrl ?? "(none)",
|
|
1268
|
+
// For browser-driven tasks the app is always Edge/Chrome. The prompt
|
|
1269
|
+
// accepts a free-form apps field; keeping it accurate matters less than
|
|
1270
|
+
// anchoring the model with non-empty context.
|
|
1271
|
+
apps: "Edge",
|
|
1272
|
+
date: new Date().toISOString().slice(0, 10),
|
|
1273
|
+
});
|
|
1274
|
+
const client = this.getClient();
|
|
1275
|
+
const response = await client.createChatCompletion({
|
|
1276
|
+
logger: this.logger,
|
|
1277
|
+
options: {
|
|
1278
|
+
messages: [
|
|
1279
|
+
{
|
|
1280
|
+
role: "system",
|
|
1281
|
+
content: "You are an expert failure analyst for computer-use web agents. Output only valid JSON conforming to the schema in the user message.",
|
|
1282
|
+
},
|
|
1283
|
+
{ role: "user", content: prompt },
|
|
1284
|
+
],
|
|
1285
|
+
response_model: {
|
|
1286
|
+
name: "TaskValidity",
|
|
1287
|
+
schema: TaskValiditySchema,
|
|
1288
|
+
},
|
|
1289
|
+
},
|
|
1290
|
+
});
|
|
1291
|
+
const data = response.data;
|
|
1292
|
+
return {
|
|
1293
|
+
isAmbiguous: data.is_ambiguous,
|
|
1294
|
+
isInvalid: data.is_invalid,
|
|
1295
|
+
ambiguityReason: data.is_ambiguous && data.reasoning_is_ambiguous
|
|
1296
|
+
? data.reasoning_is_ambiguous
|
|
1297
|
+
: undefined,
|
|
1298
|
+
invalidReason: data.is_invalid && data.reasoning_is_invalid
|
|
1299
|
+
? data.reasoning_is_invalid
|
|
1300
|
+
: undefined,
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Format the rubric with per-criterion rescored points + explanations.
|
|
1305
|
+
* The outcome verifier reads this as advisory context — it sees how a
|
|
1306
|
+
* separate scoring system viewed each criterion but forms its own result.
|
|
1307
|
+
*/
|
|
1308
|
+
formatScoredRubricSummary(rubric, scores) {
|
|
1309
|
+
return rubric.items
|
|
1310
|
+
.map((c, i) => {
|
|
1311
|
+
const cond = c.condition ? ` [condition: ${c.condition}]` : "";
|
|
1312
|
+
const score = scores[i];
|
|
1313
|
+
const earned = score?.earnedPoints ?? "—";
|
|
1314
|
+
const explanation = score?.explanation ?? "";
|
|
1315
|
+
return `${i + 1}. ${c.criterion} (${earned}/${c.maxPoints} pts)${cond}\n Description: ${c.description}\n Score explanation: ${explanation}`;
|
|
1316
|
+
})
|
|
1317
|
+
.join("\n\n");
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Compact textual action history for embedding in prompts. One line per
|
|
1321
|
+
* step. Full per-step detail lives in trajectory.json on disk.
|
|
1322
|
+
*/
|
|
1323
|
+
formatActionHistory(trajectory, config) {
|
|
1324
|
+
const reasoningLimit = config.truncation.actionHistoryReasoning;
|
|
1325
|
+
const history = trajectory.steps
|
|
1326
|
+
.map((s, i) => {
|
|
1327
|
+
const argSummary = summarizeArgs(s.actionArgs);
|
|
1328
|
+
const reasoning = (s.reasoning ?? "").slice(0, reasoningLimit);
|
|
1329
|
+
const url = s.probeEvidence.url ? ` @ ${s.probeEvidence.url}` : "";
|
|
1330
|
+
return `Step ${i}: ${s.actionName}(${argSummary})${url}${reasoning ? `\n reasoning: ${reasoning}` : ""}`;
|
|
1331
|
+
})
|
|
1332
|
+
.join("\n");
|
|
1333
|
+
return clampToTokenBudget(history, config.actionHistoryTokenBudget);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
exports.RubricVerifier = RubricVerifier;
|
|
1337
|
+
/** FIFO concurrency limiter; avoids a new dep. */
|
|
1338
|
+
function pLimit(concurrency) {
|
|
1339
|
+
const n = Math.max(1, Math.floor(concurrency));
|
|
1340
|
+
let active = 0;
|
|
1341
|
+
const queue = [];
|
|
1342
|
+
const next = () => {
|
|
1343
|
+
if (active >= n)
|
|
1344
|
+
return;
|
|
1345
|
+
const job = queue.shift();
|
|
1346
|
+
if (job) {
|
|
1347
|
+
active++;
|
|
1348
|
+
job();
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
return (fn) => new Promise((resolve, reject) => {
|
|
1352
|
+
queue.push(() => {
|
|
1353
|
+
fn()
|
|
1354
|
+
.then(resolve, reject)
|
|
1355
|
+
.finally(() => {
|
|
1356
|
+
active--;
|
|
1357
|
+
next();
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
next();
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Always-attach final visual evidence: the trajectory's terminal page
|
|
1365
|
+
* observation plus the last step's probe screenshot, deduped. Returned in
|
|
1366
|
+
* chronological order (last-step probe first if both are present, then
|
|
1367
|
+
* finalObservation as the closing frame).
|
|
1368
|
+
*
|
|
1369
|
+
* This bypasses the per-criterion top-K relevance ranking used by
|
|
1370
|
+
* {@link fusedJudgment} and the (previously) text-only outcome prompt, so the
|
|
1371
|
+
* judge LLM always sees what the page actually looked like at the end of the
|
|
1372
|
+
* run — not just whatever images happened to rank well against keyword
|
|
1373
|
+
* heuristics.
|
|
1374
|
+
*/
|
|
1375
|
+
function selectFinalImages(trajectory) {
|
|
1376
|
+
const out = [];
|
|
1377
|
+
const seen = new Set();
|
|
1378
|
+
const push = (label, bytes) => {
|
|
1379
|
+
if (!bytes || bytes.length === 0)
|
|
1380
|
+
return;
|
|
1381
|
+
const key = `${bytes.length}:${bytes.subarray(0, 32).toString("base64")}`;
|
|
1382
|
+
if (seen.has(key))
|
|
1383
|
+
return;
|
|
1384
|
+
seen.add(key);
|
|
1385
|
+
out.push({ label, bytes, mediaType: "image/png" });
|
|
1386
|
+
};
|
|
1387
|
+
const lastStepIdx = trajectory.steps.length - 1;
|
|
1388
|
+
if (lastStepIdx >= 0) {
|
|
1389
|
+
push(`step ${lastStepIdx} probe screenshot (final action)`, trajectory.steps[lastStepIdx].probeEvidence.screenshot);
|
|
1390
|
+
}
|
|
1391
|
+
push("trajectory final observation", trajectory.finalObservation?.screenshot);
|
|
1392
|
+
return out;
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Always-attach final textual evidence: the terminal page content (URL +
|
|
1396
|
+
* ariaTree) at the end of the run. Prefers `trajectory.finalObservation`
|
|
1397
|
+
* (the explicit post-run probe) and falls back to the last step's
|
|
1398
|
+
* `probeEvidence`. Clamped to {@link FINAL_STATE_ARIA_CHARS}.
|
|
1399
|
+
*
|
|
1400
|
+
* Returns an empty string when no terminal observation is available — the
|
|
1401
|
+
* caller should treat the resulting prompt section as "(no final state
|
|
1402
|
+
* captured)" rather than omit the header entirely so the LLM doesn't think
|
|
1403
|
+
* the field is missing.
|
|
1404
|
+
*/
|
|
1405
|
+
function buildFinalStateBlock(trajectory) {
|
|
1406
|
+
const last = trajectory.steps[trajectory.steps.length - 1];
|
|
1407
|
+
const sources = [];
|
|
1408
|
+
if (last?.probeEvidence) {
|
|
1409
|
+
sources.push({
|
|
1410
|
+
label: `Last step (Step ${trajectory.steps.length - 1}, post-action)`,
|
|
1411
|
+
url: last.probeEvidence.url,
|
|
1412
|
+
ariaTree: last.probeEvidence.ariaTree,
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
if (trajectory.finalObservation) {
|
|
1416
|
+
sources.push({
|
|
1417
|
+
label: "Final observation (probed after trajectory ended)",
|
|
1418
|
+
url: trajectory.finalObservation.url,
|
|
1419
|
+
ariaTree: trajectory.finalObservation.ariaTree,
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
if (sources.length === 0)
|
|
1423
|
+
return "(no final state captured)";
|
|
1424
|
+
return sources
|
|
1425
|
+
.map((s) => {
|
|
1426
|
+
const aria = s.ariaTree && s.ariaTree.length > FINAL_STATE_ARIA_CHARS
|
|
1427
|
+
? `${s.ariaTree.slice(0, FINAL_STATE_ARIA_CHARS)}\n...[truncated ${s.ariaTree.length - FINAL_STATE_ARIA_CHARS} chars]`
|
|
1428
|
+
: (s.ariaTree ?? "(no aria tree)");
|
|
1429
|
+
const url = s.url ? `\n url: ${s.url}` : "";
|
|
1430
|
+
return `${s.label}:${url}\n aria_tree:\n${aria}`;
|
|
1431
|
+
})
|
|
1432
|
+
.join("\n\n");
|
|
1433
|
+
}
|
|
1434
|
+
function selectRecentImages(trajectory, limit) {
|
|
1435
|
+
if (limit <= 0)
|
|
1436
|
+
return [];
|
|
1437
|
+
const images = [];
|
|
1438
|
+
const seen = new Set();
|
|
1439
|
+
for (let i = trajectory.steps.length - 1; i >= 0; i--) {
|
|
1440
|
+
const step = trajectory.steps[i];
|
|
1441
|
+
const candidates = [];
|
|
1442
|
+
if (step.probeEvidence.screenshot) {
|
|
1443
|
+
candidates.push({
|
|
1444
|
+
label: `step ${i} probe screenshot`,
|
|
1445
|
+
bytes: step.probeEvidence.screenshot,
|
|
1446
|
+
mediaType: "image/png",
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
for (const modality of step.agentEvidence.modalities) {
|
|
1450
|
+
if (modality.type === "image") {
|
|
1451
|
+
candidates.push({
|
|
1452
|
+
label: `step ${i} agent image`,
|
|
1453
|
+
bytes: modality.bytes,
|
|
1454
|
+
mediaType: modality.mediaType,
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
for (const candidate of candidates) {
|
|
1459
|
+
const key = `${candidate.mediaType}:${candidate.bytes.length}:${candidate.bytes.subarray(0, 32).toString("base64")}`;
|
|
1460
|
+
if (seen.has(key))
|
|
1461
|
+
continue;
|
|
1462
|
+
seen.add(key);
|
|
1463
|
+
images.push(candidate);
|
|
1464
|
+
if (images.length >= limit)
|
|
1465
|
+
return images.reverse();
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return images.reverse();
|
|
1469
|
+
}
|
|
1470
|
+
function currentDateForVerifier() {
|
|
1471
|
+
return new Date().toISOString().slice(0, 10);
|
|
1472
|
+
}
|
|
1473
|
+
const OUTCOME_KEYWORD_STOPWORDS = new Set([
|
|
1474
|
+
"about",
|
|
1475
|
+
"above",
|
|
1476
|
+
"access",
|
|
1477
|
+
"agent",
|
|
1478
|
+
"also",
|
|
1479
|
+
"answer",
|
|
1480
|
+
"available",
|
|
1481
|
+
"based",
|
|
1482
|
+
"been",
|
|
1483
|
+
"being",
|
|
1484
|
+
"browser",
|
|
1485
|
+
"class",
|
|
1486
|
+
"click",
|
|
1487
|
+
"correct",
|
|
1488
|
+
"current",
|
|
1489
|
+
"details",
|
|
1490
|
+
"final",
|
|
1491
|
+
"find",
|
|
1492
|
+
"found",
|
|
1493
|
+
"from",
|
|
1494
|
+
"have",
|
|
1495
|
+
"including",
|
|
1496
|
+
"into",
|
|
1497
|
+
"list",
|
|
1498
|
+
"located",
|
|
1499
|
+
"model",
|
|
1500
|
+
"more",
|
|
1501
|
+
"navigated",
|
|
1502
|
+
"page",
|
|
1503
|
+
"provided",
|
|
1504
|
+
"request",
|
|
1505
|
+
"requested",
|
|
1506
|
+
"results",
|
|
1507
|
+
"search",
|
|
1508
|
+
"show",
|
|
1509
|
+
"successfully",
|
|
1510
|
+
"task",
|
|
1511
|
+
"that",
|
|
1512
|
+
"the",
|
|
1513
|
+
"their",
|
|
1514
|
+
"then",
|
|
1515
|
+
"there",
|
|
1516
|
+
"this",
|
|
1517
|
+
"through",
|
|
1518
|
+
"user",
|
|
1519
|
+
"using",
|
|
1520
|
+
"which",
|
|
1521
|
+
"with",
|
|
1522
|
+
]);
|
|
1523
|
+
function outcomeKeywords(text) {
|
|
1524
|
+
const counts = new Map();
|
|
1525
|
+
for (const match of text.toLowerCase().matchAll(/[a-z0-9][a-z0-9._/-]*/g)) {
|
|
1526
|
+
const word = match[0].replace(/^[-_./]+|[-_./]+$/g, "");
|
|
1527
|
+
if (!word)
|
|
1528
|
+
continue;
|
|
1529
|
+
if (OUTCOME_KEYWORD_STOPWORDS.has(word))
|
|
1530
|
+
continue;
|
|
1531
|
+
if (word.length < 4 && !/\d/.test(word))
|
|
1532
|
+
continue;
|
|
1533
|
+
counts.set(word, (counts.get(word) ?? 0) + 1);
|
|
1534
|
+
}
|
|
1535
|
+
return [...counts.entries()]
|
|
1536
|
+
.sort((a, b) => b[1] - a[1] || b[0].length - a[0].length)
|
|
1537
|
+
.slice(0, 36)
|
|
1538
|
+
.map(([word]) => word)
|
|
1539
|
+
.sort((a, b) => b.length - a.length || a.localeCompare(b));
|
|
1540
|
+
}
|
|
1541
|
+
function bestOutcomeExcerpt(text, keywords, maxChars) {
|
|
1542
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
1543
|
+
if (compact.length <= maxChars)
|
|
1544
|
+
return compact;
|
|
1545
|
+
const lower = compact.toLowerCase();
|
|
1546
|
+
let bestIndex = -1;
|
|
1547
|
+
for (const keyword of keywords) {
|
|
1548
|
+
const idx = lower.indexOf(keyword);
|
|
1549
|
+
if (idx >= 0) {
|
|
1550
|
+
bestIndex = idx;
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
if (bestIndex < 0) {
|
|
1555
|
+
return `${compact.slice(0, maxChars)}... [truncated]`;
|
|
1556
|
+
}
|
|
1557
|
+
const before = Math.floor(maxChars * 0.35);
|
|
1558
|
+
const start = Math.max(0, bestIndex - before);
|
|
1559
|
+
const end = Math.min(compact.length, start + maxChars);
|
|
1560
|
+
const prefix = start > 0 ? "... " : "";
|
|
1561
|
+
const suffix = end < compact.length ? " ... [truncated]" : "";
|
|
1562
|
+
return `${prefix}${compact.slice(start, end)}${suffix}`;
|
|
1563
|
+
}
|
|
1564
|
+
function clampToTokenBudget(text, tokenBudget) {
|
|
1565
|
+
const maxChars = Math.max(0, tokenBudget) * APPROX_CHARS_PER_TOKEN;
|
|
1566
|
+
if (maxChars === 0 || text.length <= maxChars)
|
|
1567
|
+
return text;
|
|
1568
|
+
const keepHead = Math.floor(maxChars * 0.35);
|
|
1569
|
+
const keepTail = Math.max(0, maxChars - keepHead - 120);
|
|
1570
|
+
return [
|
|
1571
|
+
text.slice(0, keepHead).trimEnd(),
|
|
1572
|
+
`\n...[truncated ${text.length - keepHead - keepTail} chars to fit verifier context budget]...\n`,
|
|
1573
|
+
text.slice(text.length - keepTail).trimStart(),
|
|
1574
|
+
].join("");
|
|
1575
|
+
}
|
|
1576
|
+
function filterByTaskSpan(items, taskInstruction, logger) {
|
|
1577
|
+
const normalizedTask = normalizeForSpanMatch(taskInstruction);
|
|
1578
|
+
const kept = [];
|
|
1579
|
+
const dropped = [];
|
|
1580
|
+
for (const item of items) {
|
|
1581
|
+
const span = item.task_span?.trim();
|
|
1582
|
+
if (!span) {
|
|
1583
|
+
dropped.push({
|
|
1584
|
+
criterion: item.criterion,
|
|
1585
|
+
reason: "missing task_span",
|
|
1586
|
+
});
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
if (span === "<critical-point>" ||
|
|
1590
|
+
normalizedTask.includes(normalizeForSpanMatch(span))) {
|
|
1591
|
+
kept.push(item);
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
dropped.push({
|
|
1595
|
+
criterion: item.criterion,
|
|
1596
|
+
reason: `task_span ${JSON.stringify(span)} not found in task instruction`,
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
if (dropped.length > 0) {
|
|
1600
|
+
logger({
|
|
1601
|
+
category: "v3-evaluator",
|
|
1602
|
+
message: "rubric: dropped hallucinated criteria via task_span filter",
|
|
1603
|
+
auxiliary: {
|
|
1604
|
+
droppedCount: { value: String(dropped.length), type: "integer" },
|
|
1605
|
+
dropped: {
|
|
1606
|
+
value: JSON.stringify(dropped),
|
|
1607
|
+
type: "object",
|
|
1608
|
+
},
|
|
1609
|
+
},
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
return kept;
|
|
1613
|
+
}
|
|
1614
|
+
function normalizeForSpanMatch(value) {
|
|
1615
|
+
return value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1616
|
+
}
|
|
1617
|
+
function safeJsonSnippet(value, maxChars) {
|
|
1618
|
+
let raw;
|
|
1619
|
+
try {
|
|
1620
|
+
raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
1621
|
+
}
|
|
1622
|
+
catch {
|
|
1623
|
+
raw = String(value);
|
|
1624
|
+
}
|
|
1625
|
+
if (raw === undefined)
|
|
1626
|
+
return "(undefined)";
|
|
1627
|
+
return raw.length > maxChars ? `${raw.slice(0, maxChars)}...` : raw;
|
|
1628
|
+
}
|
|
1629
|
+
function summarizeArgs(args) {
|
|
1630
|
+
const keys = Object.keys(args);
|
|
1631
|
+
if (keys.length === 0)
|
|
1632
|
+
return "";
|
|
1633
|
+
return keys
|
|
1634
|
+
.slice(0, 3)
|
|
1635
|
+
.map((k) => {
|
|
1636
|
+
const v = args[k];
|
|
1637
|
+
if (typeof v === "string")
|
|
1638
|
+
return `${k}: ${v.slice(0, 60)}`;
|
|
1639
|
+
if (typeof v === "number" || typeof v === "boolean")
|
|
1640
|
+
return `${k}: ${v}`;
|
|
1641
|
+
return `${k}: ${typeof v}`;
|
|
1642
|
+
})
|
|
1643
|
+
.join(", ");
|
|
1644
|
+
}
|
|
1645
|
+
//# sourceMappingURL=rubricVerifier.js.map
|