@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.
Files changed (186) hide show
  1. package/README.md +0 -12
  2. package/dist/cjs/lib/utils.d.ts +6 -0
  3. package/dist/cjs/lib/utils.js +11 -0
  4. package/dist/cjs/lib/utils.js.map +1 -1
  5. package/dist/cjs/lib/v3/agent/AgentProvider.js +4 -3
  6. package/dist/cjs/lib/v3/agent/AgentProvider.js.map +1 -1
  7. package/dist/cjs/lib/v3/agent/AnthropicCUAClient.js +10 -9
  8. package/dist/cjs/lib/v3/agent/AnthropicCUAClient.js.map +1 -1
  9. package/dist/cjs/lib/v3/agent/GoogleCUAClient.js +124 -28
  10. package/dist/cjs/lib/v3/agent/GoogleCUAClient.js.map +1 -1
  11. package/dist/cjs/lib/v3/agent/utils/handleDoneToolCall.js +10 -1
  12. package/dist/cjs/lib/v3/agent/utils/handleDoneToolCall.js.map +1 -1
  13. package/dist/cjs/lib/v3/cache/ActCache.js +1 -1
  14. package/dist/cjs/lib/v3/cache/ActCache.js.map +1 -1
  15. package/dist/cjs/lib/v3/cache/utils.d.ts +7 -0
  16. package/dist/cjs/lib/v3/cache/utils.js +19 -0
  17. package/dist/cjs/lib/v3/cache/utils.js.map +1 -1
  18. package/dist/cjs/lib/v3/handlers/v3AgentHandler.d.ts +2 -1
  19. package/dist/cjs/lib/v3/handlers/v3AgentHandler.js +25 -9
  20. package/dist/cjs/lib/v3/handlers/v3AgentHandler.js.map +1 -1
  21. package/dist/cjs/lib/v3/index.d.ts +2 -2
  22. package/dist/cjs/lib/v3/index.js.map +1 -1
  23. package/dist/cjs/lib/v3/launch/local.js +1 -0
  24. package/dist/cjs/lib/v3/launch/local.js.map +1 -1
  25. package/dist/cjs/lib/v3/llm/LLMProvider.js +1 -0
  26. package/dist/cjs/lib/v3/llm/LLMProvider.js.map +1 -1
  27. package/dist/cjs/lib/v3/llm/aisdk.js +4 -0
  28. package/dist/cjs/lib/v3/llm/aisdk.js.map +1 -1
  29. package/dist/cjs/lib/v3/llm/anthropicOptions.d.ts +57 -0
  30. package/dist/cjs/lib/v3/llm/anthropicOptions.js +115 -0
  31. package/dist/cjs/lib/v3/llm/anthropicOptions.js.map +1 -0
  32. package/dist/cjs/lib/v3/types/public/agent.d.ts +1 -1
  33. package/dist/cjs/lib/v3/types/public/agent.js +2 -0
  34. package/dist/cjs/lib/v3/types/public/agent.js.map +1 -1
  35. package/dist/cjs/lib/v3/types/public/model.d.ts +3 -2
  36. package/dist/cjs/lib/v3/types/public/model.js.map +1 -1
  37. package/dist/cjs/lib/v3/understudy/page.js +1 -1
  38. package/dist/cjs/lib/v3/understudy/page.js.map +1 -1
  39. package/dist/cjs/lib/v3/v3.js +4 -1
  40. package/dist/cjs/lib/v3/v3.js.map +1 -1
  41. package/dist/cjs/lib/v3/verifier/errorTaxonomy.d.ts +35 -0
  42. package/dist/cjs/lib/v3/verifier/errorTaxonomy.js +291 -0
  43. package/dist/cjs/lib/v3/verifier/errorTaxonomy.js.map +1 -0
  44. package/dist/cjs/lib/v3/verifier/evidence.d.ts +44 -0
  45. package/dist/cjs/lib/v3/verifier/evidence.js +432 -0
  46. package/dist/cjs/lib/v3/verifier/evidence.js.map +1 -0
  47. package/dist/cjs/lib/v3/verifier/index.d.ts +1 -1
  48. package/dist/cjs/lib/v3/verifier/index.js.map +1 -1
  49. package/dist/cjs/lib/v3/verifier/prompts/batchedRelevance.d.ts +21 -0
  50. package/dist/cjs/lib/v3/verifier/prompts/batchedRelevance.js +71 -0
  51. package/dist/cjs/lib/v3/verifier/prompts/batchedRelevance.js.map +1 -0
  52. package/dist/cjs/lib/v3/verifier/prompts/firstPointOfFailure.d.ts +3 -0
  53. package/dist/cjs/lib/v3/verifier/prompts/firstPointOfFailure.js +166 -0
  54. package/dist/cjs/lib/v3/verifier/prompts/firstPointOfFailure.js.map +1 -0
  55. package/dist/cjs/lib/v3/verifier/prompts/fusedJudgment.d.ts +35 -0
  56. package/dist/cjs/lib/v3/verifier/prompts/fusedJudgment.js +158 -0
  57. package/dist/cjs/lib/v3/verifier/prompts/fusedJudgment.js.map +1 -0
  58. package/dist/cjs/lib/v3/verifier/prompts/fusedOutcome.d.ts +26 -0
  59. package/dist/cjs/lib/v3/verifier/prompts/fusedOutcome.js +137 -0
  60. package/dist/cjs/lib/v3/verifier/prompts/fusedOutcome.js.map +1 -0
  61. package/dist/cjs/lib/v3/verifier/prompts/index.d.ts +8 -0
  62. package/dist/cjs/lib/v3/verifier/prompts/index.js +22 -0
  63. package/dist/cjs/lib/v3/verifier/prompts/index.js.map +1 -0
  64. package/dist/cjs/lib/v3/verifier/prompts/perCriterionScore.d.ts +23 -0
  65. package/dist/cjs/lib/v3/verifier/prompts/perCriterionScore.js +77 -0
  66. package/dist/cjs/lib/v3/verifier/prompts/perCriterionScore.js.map +1 -0
  67. package/dist/cjs/lib/v3/verifier/prompts/render.d.ts +23 -0
  68. package/dist/cjs/lib/v3/verifier/prompts/render.js +45 -0
  69. package/dist/cjs/lib/v3/verifier/prompts/render.js.map +1 -0
  70. package/dist/cjs/lib/v3/verifier/prompts/rubricGeneration.d.ts +16 -0
  71. package/dist/cjs/lib/v3/verifier/prompts/rubricGeneration.js +321 -0
  72. package/dist/cjs/lib/v3/verifier/prompts/rubricGeneration.js.map +1 -0
  73. package/dist/cjs/lib/v3/verifier/prompts/taskValidity.d.ts +1 -0
  74. package/dist/cjs/lib/v3/verifier/prompts/taskValidity.js +116 -0
  75. package/dist/cjs/lib/v3/verifier/prompts/taskValidity.js.map +1 -0
  76. package/dist/cjs/lib/v3/verifier/rubricVerifier.d.ts +91 -0
  77. package/dist/cjs/lib/v3/verifier/rubricVerifier.js +1645 -0
  78. package/dist/cjs/lib/v3/verifier/rubricVerifier.js.map +1 -0
  79. package/dist/cjs/lib/v3/verifier/trajectory.d.ts +3 -3
  80. package/dist/cjs/lib/v3/verifier/trajectory.js +26 -26
  81. package/dist/cjs/lib/v3/verifier/trajectory.js.map +1 -1
  82. package/dist/cjs/lib/v3/verifier/types.d.ts +163 -15
  83. package/dist/cjs/lib/v3/verifier/types.js +0 -7
  84. package/dist/cjs/lib/v3/verifier/types.js.map +1 -1
  85. package/dist/cjs/lib/v3Evaluator.d.ts +5 -0
  86. package/dist/cjs/lib/v3Evaluator.js +47 -7
  87. package/dist/cjs/lib/v3Evaluator.js.map +1 -1
  88. package/dist/cjs/lib/version.d.ts +1 -1
  89. package/dist/cjs/lib/version.js +1 -1
  90. package/dist/cjs/lib/version.js.map +1 -1
  91. package/dist/esm/lib/utils.d.ts +6 -0
  92. package/dist/esm/lib/utils.js +10 -0
  93. package/dist/esm/lib/utils.js.map +1 -1
  94. package/dist/esm/lib/v3/agent/AgentProvider.js +4 -3
  95. package/dist/esm/lib/v3/agent/AgentProvider.js.map +1 -1
  96. package/dist/esm/lib/v3/agent/AnthropicCUAClient.js +10 -9
  97. package/dist/esm/lib/v3/agent/AnthropicCUAClient.js.map +1 -1
  98. package/dist/esm/lib/v3/agent/GoogleCUAClient.js +124 -28
  99. package/dist/esm/lib/v3/agent/GoogleCUAClient.js.map +1 -1
  100. package/dist/esm/lib/v3/agent/utils/handleDoneToolCall.js +10 -1
  101. package/dist/esm/lib/v3/agent/utils/handleDoneToolCall.js.map +1 -1
  102. package/dist/esm/lib/v3/cache/ActCache.js +2 -2
  103. package/dist/esm/lib/v3/cache/ActCache.js.map +1 -1
  104. package/dist/esm/lib/v3/cache/utils.d.ts +7 -0
  105. package/dist/esm/lib/v3/cache/utils.js +18 -0
  106. package/dist/esm/lib/v3/cache/utils.js.map +1 -1
  107. package/dist/esm/lib/v3/handlers/v3AgentHandler.d.ts +2 -1
  108. package/dist/esm/lib/v3/handlers/v3AgentHandler.js +25 -9
  109. package/dist/esm/lib/v3/handlers/v3AgentHandler.js.map +1 -1
  110. package/dist/esm/lib/v3/index.d.ts +2 -2
  111. package/dist/esm/lib/v3/index.js.map +1 -1
  112. package/dist/esm/lib/v3/launch/local.js +1 -0
  113. package/dist/esm/lib/v3/launch/local.js.map +1 -1
  114. package/dist/esm/lib/v3/llm/LLMProvider.js +1 -0
  115. package/dist/esm/lib/v3/llm/LLMProvider.js.map +1 -1
  116. package/dist/esm/lib/v3/llm/aisdk.js +4 -0
  117. package/dist/esm/lib/v3/llm/aisdk.js.map +1 -1
  118. package/dist/esm/lib/v3/llm/anthropicOptions.d.ts +57 -0
  119. package/dist/esm/lib/v3/llm/anthropicOptions.js +106 -0
  120. package/dist/esm/lib/v3/llm/anthropicOptions.js.map +1 -0
  121. package/dist/esm/lib/v3/types/public/agent.d.ts +1 -1
  122. package/dist/esm/lib/v3/types/public/agent.js +2 -0
  123. package/dist/esm/lib/v3/types/public/agent.js.map +1 -1
  124. package/dist/esm/lib/v3/types/public/model.d.ts +3 -2
  125. package/dist/esm/lib/v3/types/public/model.js.map +1 -1
  126. package/dist/esm/lib/v3/understudy/page.js +1 -1
  127. package/dist/esm/lib/v3/understudy/page.js.map +1 -1
  128. package/dist/esm/lib/v3/v3.js +4 -1
  129. package/dist/esm/lib/v3/v3.js.map +1 -1
  130. package/dist/esm/lib/v3/verifier/errorTaxonomy.d.ts +35 -0
  131. package/dist/esm/lib/v3/verifier/errorTaxonomy.js +285 -0
  132. package/dist/esm/lib/v3/verifier/errorTaxonomy.js.map +1 -0
  133. package/dist/esm/lib/v3/verifier/evidence.d.ts +44 -0
  134. package/dist/esm/lib/v3/verifier/evidence.js +393 -0
  135. package/dist/esm/lib/v3/verifier/evidence.js.map +1 -0
  136. package/dist/esm/lib/v3/verifier/index.d.ts +1 -1
  137. package/dist/esm/lib/v3/verifier/index.js.map +1 -1
  138. package/dist/esm/lib/v3/verifier/prompts/batchedRelevance.d.ts +21 -0
  139. package/dist/esm/lib/v3/verifier/prompts/batchedRelevance.js +68 -0
  140. package/dist/esm/lib/v3/verifier/prompts/batchedRelevance.js.map +1 -0
  141. package/dist/esm/lib/v3/verifier/prompts/firstPointOfFailure.d.ts +3 -0
  142. package/dist/esm/lib/v3/verifier/prompts/firstPointOfFailure.js +162 -0
  143. package/dist/esm/lib/v3/verifier/prompts/firstPointOfFailure.js.map +1 -0
  144. package/dist/esm/lib/v3/verifier/prompts/fusedJudgment.d.ts +35 -0
  145. package/dist/esm/lib/v3/verifier/prompts/fusedJudgment.js +155 -0
  146. package/dist/esm/lib/v3/verifier/prompts/fusedJudgment.js.map +1 -0
  147. package/dist/esm/lib/v3/verifier/prompts/fusedOutcome.d.ts +26 -0
  148. package/dist/esm/lib/v3/verifier/prompts/fusedOutcome.js +134 -0
  149. package/dist/esm/lib/v3/verifier/prompts/fusedOutcome.js.map +1 -0
  150. package/dist/esm/lib/v3/verifier/prompts/index.d.ts +8 -0
  151. package/dist/esm/lib/v3/verifier/prompts/index.js +9 -0
  152. package/dist/esm/lib/v3/verifier/prompts/index.js.map +1 -0
  153. package/dist/esm/lib/v3/verifier/prompts/perCriterionScore.d.ts +23 -0
  154. package/dist/esm/lib/v3/verifier/prompts/perCriterionScore.js +74 -0
  155. package/dist/esm/lib/v3/verifier/prompts/perCriterionScore.js.map +1 -0
  156. package/dist/esm/lib/v3/verifier/prompts/render.d.ts +23 -0
  157. package/dist/esm/lib/v3/verifier/prompts/render.js +41 -0
  158. package/dist/esm/lib/v3/verifier/prompts/render.js.map +1 -0
  159. package/dist/esm/lib/v3/verifier/prompts/rubricGeneration.d.ts +16 -0
  160. package/dist/esm/lib/v3/verifier/prompts/rubricGeneration.js +318 -0
  161. package/dist/esm/lib/v3/verifier/prompts/rubricGeneration.js.map +1 -0
  162. package/dist/esm/lib/v3/verifier/prompts/taskValidity.d.ts +1 -0
  163. package/dist/esm/lib/v3/verifier/prompts/taskValidity.js +113 -0
  164. package/dist/esm/lib/v3/verifier/prompts/taskValidity.js.map +1 -0
  165. package/dist/esm/lib/v3/verifier/rubricVerifier.d.ts +91 -0
  166. package/dist/esm/lib/v3/verifier/rubricVerifier.js +1640 -0
  167. package/dist/esm/lib/v3/verifier/rubricVerifier.js.map +1 -0
  168. package/dist/esm/lib/v3/verifier/trajectory.d.ts +3 -3
  169. package/dist/esm/lib/v3/verifier/trajectory.js +26 -26
  170. package/dist/esm/lib/v3/verifier/trajectory.js.map +1 -1
  171. package/dist/esm/lib/v3/verifier/types.d.ts +163 -15
  172. package/dist/esm/lib/v3/verifier/types.js +0 -7
  173. package/dist/esm/lib/v3/verifier/types.js.map +1 -1
  174. package/dist/esm/lib/v3Evaluator.d.ts +5 -0
  175. package/dist/esm/lib/v3Evaluator.js +47 -7
  176. package/dist/esm/lib/v3Evaluator.js.map +1 -1
  177. package/dist/esm/lib/version.d.ts +1 -1
  178. package/dist/esm/lib/version.js +1 -1
  179. package/dist/esm/lib/version.js.map +1 -1
  180. package/package.json +8 -8
  181. package/dist/cjs/lib/v3/dom/build/selectorRuntime.generated.d.ts +0 -24
  182. package/dist/cjs/lib/v3/dom/build/selectorRuntime.generated.js +0 -31
  183. package/dist/cjs/lib/v3/dom/build/selectorRuntime.generated.js.map +0 -1
  184. package/dist/esm/lib/v3/dom/build/selectorRuntime.generated.d.ts +0 -24
  185. package/dist/esm/lib/v3/dom/build/selectorRuntime.generated.js +0 -28
  186. 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