ai-runtime-engine 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +15 -0
  3. package/README.md +489 -0
  4. package/dist/artifacts/artifacts.d.ts +28 -0
  5. package/dist/artifacts/artifacts.js +46 -0
  6. package/dist/benchmark/benchmark.d.ts +23 -0
  7. package/dist/benchmark/benchmark.js +40 -0
  8. package/dist/cli/cli.d.ts +6 -0
  9. package/dist/cli/cli.js +161 -0
  10. package/dist/cli/commands/cleanup.d.ts +47 -0
  11. package/dist/cli/commands/cleanup.js +96 -0
  12. package/dist/cli/commands/config.d.ts +8 -0
  13. package/dist/cli/commands/config.js +28 -0
  14. package/dist/cli/commands/doctor.d.ts +57 -0
  15. package/dist/cli/commands/doctor.js +86 -0
  16. package/dist/cli/commands/executions.d.ts +9 -0
  17. package/dist/cli/commands/executions.js +25 -0
  18. package/dist/cli/commands/info.d.ts +43 -0
  19. package/dist/cli/commands/info.js +53 -0
  20. package/dist/cli/commands/init.d.ts +5 -0
  21. package/dist/cli/commands/init.js +75 -0
  22. package/dist/cli/commands/inspect.d.ts +16 -0
  23. package/dist/cli/commands/inspect.js +60 -0
  24. package/dist/cli/commands/phase2.d.ts +22 -0
  25. package/dist/cli/commands/phase2.js +83 -0
  26. package/dist/cli/commands/route.d.ts +14 -0
  27. package/dist/cli/commands/route.js +49 -0
  28. package/dist/cli/commands/run.d.ts +11 -0
  29. package/dist/cli/commands/run.js +37 -0
  30. package/dist/cli/commands/setup.d.ts +34 -0
  31. package/dist/cli/commands/setup.js +104 -0
  32. package/dist/cli/commands/skills.d.ts +28 -0
  33. package/dist/cli/commands/skills.js +48 -0
  34. package/dist/cli/commands/test.d.ts +7 -0
  35. package/dist/cli/commands/test.js +29 -0
  36. package/dist/cli/context.d.ts +12 -0
  37. package/dist/cli/context.js +16 -0
  38. package/dist/cli/interactive/repl.d.ts +6 -0
  39. package/dist/cli/interactive/repl.js +45 -0
  40. package/dist/cli/interactive/session.d.ts +36 -0
  41. package/dist/cli/interactive/session.js +356 -0
  42. package/dist/cli/prompt.d.ts +6 -0
  43. package/dist/cli/prompt.js +18 -0
  44. package/dist/cli/render.d.ts +7 -0
  45. package/dist/cli/render.js +14 -0
  46. package/dist/comparison/analysis.d.ts +46 -0
  47. package/dist/comparison/analysis.js +177 -0
  48. package/dist/comparison/comparator.d.ts +46 -0
  49. package/dist/comparison/comparator.js +270 -0
  50. package/dist/comparison/comparison.d.ts +140 -0
  51. package/dist/comparison/comparison.js +9 -0
  52. package/dist/comparison/render.d.ts +7 -0
  53. package/dist/comparison/render.js +66 -0
  54. package/dist/config/defaults.d.ts +52 -0
  55. package/dist/config/defaults.js +56 -0
  56. package/dist/config/load.d.ts +17 -0
  57. package/dist/config/load.js +50 -0
  58. package/dist/config/providerDefaults.d.ts +17 -0
  59. package/dist/config/providerDefaults.js +61 -0
  60. package/dist/config/schema.d.ts +9 -0
  61. package/dist/config/schema.js +78 -0
  62. package/dist/context/budget.d.ts +13 -0
  63. package/dist/context/budget.js +17 -0
  64. package/dist/context/compiler.d.ts +61 -0
  65. package/dist/context/compiler.js +125 -0
  66. package/dist/context/tokens.d.ts +19 -0
  67. package/dist/context/tokens.js +38 -0
  68. package/dist/conversations/conversations.d.ts +38 -0
  69. package/dist/conversations/conversations.js +64 -0
  70. package/dist/core/capabilities/evidence.d.ts +40 -0
  71. package/dist/core/capabilities/evidence.js +102 -0
  72. package/dist/core/capabilities/overlay.d.ts +15 -0
  73. package/dist/core/capabilities/overlay.js +0 -0
  74. package/dist/core/capabilities/taxonomy.d.ts +19 -0
  75. package/dist/core/capabilities/taxonomy.js +25 -0
  76. package/dist/core/fallback/errors.d.ts +30 -0
  77. package/dist/core/fallback/errors.js +80 -0
  78. package/dist/core/fallback/fallback.d.ts +40 -0
  79. package/dist/core/fallback/fallback.js +82 -0
  80. package/dist/core/fallback/retryPolicy.d.ts +11 -0
  81. package/dist/core/fallback/retryPolicy.js +14 -0
  82. package/dist/core/health/health.d.ts +3 -0
  83. package/dist/core/health/health.js +5 -0
  84. package/dist/core/health/monitor.d.ts +23 -0
  85. package/dist/core/health/monitor.js +82 -0
  86. package/dist/core/policies/budget.d.ts +19 -0
  87. package/dist/core/policies/budget.js +37 -0
  88. package/dist/core/registry/builtinTasks.d.ts +8 -0
  89. package/dist/core/registry/builtinTasks.js +54 -0
  90. package/dist/core/registry/registry.d.ts +18 -0
  91. package/dist/core/registry/registry.js +33 -0
  92. package/dist/core/registry/taskRegistry.d.ts +15 -0
  93. package/dist/core/registry/taskRegistry.js +30 -0
  94. package/dist/core/router/confidence.d.ts +7 -0
  95. package/dist/core/router/confidence.js +20 -0
  96. package/dist/core/router/dimensions.d.ts +16 -0
  97. package/dist/core/router/dimensions.js +60 -0
  98. package/dist/core/router/executor.d.ts +16 -0
  99. package/dist/core/router/executor.js +25 -0
  100. package/dist/core/router/filter.d.ts +34 -0
  101. package/dist/core/router/filter.js +113 -0
  102. package/dist/core/router/normalize.d.ts +30 -0
  103. package/dist/core/router/normalize.js +119 -0
  104. package/dist/core/router/request.d.ts +4 -0
  105. package/dist/core/router/request.js +21 -0
  106. package/dist/core/router/router.d.ts +32 -0
  107. package/dist/core/router/router.js +195 -0
  108. package/dist/core/router/routingPrefs.d.ts +11 -0
  109. package/dist/core/router/routingPrefs.js +30 -0
  110. package/dist/core/router/scorer.d.ts +19 -0
  111. package/dist/core/router/scorer.js +50 -0
  112. package/dist/core/router/weights.d.ts +9 -0
  113. package/dist/core/router/weights.js +31 -0
  114. package/dist/core/validation/validator.d.ts +16 -0
  115. package/dist/core/validation/validator.js +33 -0
  116. package/dist/discovery/modelCatalog.d.ts +28 -0
  117. package/dist/discovery/modelCatalog.js +105 -0
  118. package/dist/discovery/openapi.d.ts +25 -0
  119. package/dist/discovery/openapi.js +76 -0
  120. package/dist/executions/checkpoint.d.ts +26 -0
  121. package/dist/executions/checkpoint.js +114 -0
  122. package/dist/executions/execution.d.ts +51 -0
  123. package/dist/executions/execution.js +8 -0
  124. package/dist/executions/store.d.ts +52 -0
  125. package/dist/executions/store.js +124 -0
  126. package/dist/generation/generateAdapter.d.ts +17 -0
  127. package/dist/generation/generateAdapter.js +30 -0
  128. package/dist/index.d.ts +147 -0
  129. package/dist/index.js +107 -0
  130. package/dist/learning/feedback.d.ts +9 -0
  131. package/dist/learning/feedback.js +18 -0
  132. package/dist/learning/learningStore.d.ts +68 -0
  133. package/dist/learning/learningStore.js +138 -0
  134. package/dist/learning/performanceStore.d.ts +27 -0
  135. package/dist/learning/performanceStore.js +0 -0
  136. package/dist/marketplace/presets.d.ts +24 -0
  137. package/dist/marketplace/presets.js +52 -0
  138. package/dist/mcp/mcp.d.ts +31 -0
  139. package/dist/mcp/mcp.js +54 -0
  140. package/dist/memory/bm25.d.ts +16 -0
  141. package/dist/memory/bm25.js +56 -0
  142. package/dist/memory/classifier.d.ts +14 -0
  143. package/dist/memory/classifier.js +17 -0
  144. package/dist/memory/memory.d.ts +80 -0
  145. package/dist/memory/memory.js +191 -0
  146. package/dist/orchestration/executor.d.ts +35 -0
  147. package/dist/orchestration/executor.js +65 -0
  148. package/dist/orchestration/orchestrator.d.ts +42 -0
  149. package/dist/orchestration/orchestrator.js +63 -0
  150. package/dist/orchestration/plan.d.ts +37 -0
  151. package/dist/orchestration/plan.js +70 -0
  152. package/dist/orchestration/planner.d.ts +29 -0
  153. package/dist/orchestration/planner.js +69 -0
  154. package/dist/plugin/ai.d.ts +82 -0
  155. package/dist/plugin/ai.js +167 -0
  156. package/dist/probing/probe.d.ts +25 -0
  157. package/dist/probing/probe.js +63 -0
  158. package/dist/providers/factory.d.ts +18 -0
  159. package/dist/providers/factory.js +54 -0
  160. package/dist/providers/httpClient.d.ts +34 -0
  161. package/dist/providers/httpClient.js +80 -0
  162. package/dist/providers/httpProvider.d.ts +49 -0
  163. package/dist/providers/httpProvider.js +135 -0
  164. package/dist/providers/mock/demo.d.ts +13 -0
  165. package/dist/providers/mock/demo.js +58 -0
  166. package/dist/providers/mock/mockProvider.d.ts +35 -0
  167. package/dist/providers/mock/mockProvider.js +121 -0
  168. package/dist/providers/mock/scenarios.d.ts +44 -0
  169. package/dist/providers/mock/scenarios.js +30 -0
  170. package/dist/providers/provider.d.ts +26 -0
  171. package/dist/providers/provider.js +11 -0
  172. package/dist/providers/wire/anthropicWire.d.ts +6 -0
  173. package/dist/providers/wire/anthropicWire.js +83 -0
  174. package/dist/providers/wire/openaiWire.d.ts +7 -0
  175. package/dist/providers/wire/openaiWire.js +81 -0
  176. package/dist/providers/wire/registry.d.ts +8 -0
  177. package/dist/providers/wire/registry.js +20 -0
  178. package/dist/providers/wire/types.d.ts +39 -0
  179. package/dist/providers/wire/types.js +24 -0
  180. package/dist/runtime/config.d.ts +31 -0
  181. package/dist/runtime/config.js +121 -0
  182. package/dist/runtime/context.d.ts +34 -0
  183. package/dist/runtime/context.js +11 -0
  184. package/dist/runtime/events.d.ts +99 -0
  185. package/dist/runtime/events.js +82 -0
  186. package/dist/runtime/host.d.ts +27 -0
  187. package/dist/runtime/host.js +7 -0
  188. package/dist/runtime/intent/classifier.d.ts +30 -0
  189. package/dist/runtime/intent/classifier.js +60 -0
  190. package/dist/runtime/intent/signals.d.ts +19 -0
  191. package/dist/runtime/intent/signals.js +46 -0
  192. package/dist/runtime/modes/availability.d.ts +11 -0
  193. package/dist/runtime/modes/availability.js +17 -0
  194. package/dist/runtime/modes/chat.d.ts +18 -0
  195. package/dist/runtime/modes/chat.js +67 -0
  196. package/dist/runtime/modes/modeResolver.d.ts +43 -0
  197. package/dist/runtime/modes/modeResolver.js +78 -0
  198. package/dist/runtime/policy.d.ts +72 -0
  199. package/dist/runtime/policy.js +59 -0
  200. package/dist/runtime/providerView.d.ts +62 -0
  201. package/dist/runtime/providerView.js +105 -0
  202. package/dist/runtime/routing.d.ts +26 -0
  203. package/dist/runtime/routing.js +65 -0
  204. package/dist/runtime/runtime.d.ts +191 -0
  205. package/dist/runtime/runtime.js +718 -0
  206. package/dist/runtime/types.d.ts +153 -0
  207. package/dist/runtime/types.js +9 -0
  208. package/dist/runtime/workspace/detectors.d.ts +15 -0
  209. package/dist/runtime/workspace/detectors.js +57 -0
  210. package/dist/runtime/workspace/workspace.d.ts +29 -0
  211. package/dist/runtime/workspace/workspace.js +116 -0
  212. package/dist/security/credentials.d.ts +26 -0
  213. package/dist/security/credentials.js +34 -0
  214. package/dist/security/redact.d.ts +16 -0
  215. package/dist/security/redact.js +57 -0
  216. package/dist/skills/builtins/fileAnalyzer.d.ts +7 -0
  217. package/dist/skills/builtins/fileAnalyzer.js +47 -0
  218. package/dist/skills/builtins/repositoryAnalyzer.d.ts +6 -0
  219. package/dist/skills/builtins/repositoryAnalyzer.js +47 -0
  220. package/dist/skills/discovery.d.ts +61 -0
  221. package/dist/skills/discovery.js +211 -0
  222. package/dist/skills/manifest.d.ts +30 -0
  223. package/dist/skills/manifest.js +75 -0
  224. package/dist/skills/registry.d.ts +15 -0
  225. package/dist/skills/registry.js +22 -0
  226. package/dist/skills/skill.d.ts +64 -0
  227. package/dist/skills/skill.js +8 -0
  228. package/dist/store/area.d.ts +54 -0
  229. package/dist/store/area.js +164 -0
  230. package/dist/store/paths.d.ts +15 -0
  231. package/dist/store/paths.js +48 -0
  232. package/dist/store/store.d.ts +59 -0
  233. package/dist/store/store.js +140 -0
  234. package/dist/telemetry/sinks/file.d.ts +12 -0
  235. package/dist/telemetry/sinks/file.js +28 -0
  236. package/dist/telemetry/telemetry.d.ts +36 -0
  237. package/dist/telemetry/telemetry.js +63 -0
  238. package/dist/tools/builtins/filesystem.d.ts +7 -0
  239. package/dist/tools/builtins/filesystem.js +53 -0
  240. package/dist/tools/builtins/git.d.ts +10 -0
  241. package/dist/tools/builtins/git.js +66 -0
  242. package/dist/tools/builtins/shell.d.ts +17 -0
  243. package/dist/tools/builtins/shell.js +91 -0
  244. package/dist/tools/jail.d.ts +12 -0
  245. package/dist/tools/jail.js +98 -0
  246. package/dist/tools/permissions.d.ts +25 -0
  247. package/dist/tools/permissions.js +24 -0
  248. package/dist/tools/registry.d.ts +10 -0
  249. package/dist/tools/registry.js +20 -0
  250. package/dist/tools/runner.d.ts +23 -0
  251. package/dist/tools/runner.js +64 -0
  252. package/dist/tools/tool.d.ts +53 -0
  253. package/dist/tools/tool.js +24 -0
  254. package/dist/tools/untrusted.d.ts +13 -0
  255. package/dist/tools/untrusted.js +30 -0
  256. package/dist/types.d.ts +460 -0
  257. package/dist/types.js +12 -0
  258. package/dist/util/clock.d.ts +6 -0
  259. package/dist/util/clock.js +4 -0
  260. package/dist/util/extractJson.d.ts +8 -0
  261. package/dist/util/extractJson.js +54 -0
  262. package/dist/verification/verify.d.ts +26 -0
  263. package/dist/verification/verify.js +67 -0
  264. package/docs/GUIDE.md +358 -0
  265. package/docs/README.md +21 -0
  266. package/docs/architecture.md +78 -0
  267. package/docs/router.md +376 -0
  268. package/docs/security.md +55 -0
  269. package/package.json +67 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Deterministic comparison analysis — no model calls, no randomness. Outputs are compared by token-set
3
+ * similarity (reusing the memory tokenizer), clustered into agreement groups, diffed pairwise, checked
4
+ * for unique ("missing from others") sentences, and — for structured (JSON) outputs — checked for direct
5
+ * field-level contradictions. Everything here is a pure function of the candidates, so it is fully unit
6
+ * testable and never fabricates a contradiction it cannot ground (free-text contradictions are left to a
7
+ * judge; only structured fields are asserted as contradictions).
8
+ */
9
+ import { tokenize } from '../memory/bm25.js';
10
+ /** Token-set Jaccard similarity of two token bags, 0..1. Two empty bags count as identical (1). */
11
+ export function jaccard(a, b) {
12
+ const sa = new Set(a);
13
+ const sb = new Set(b);
14
+ if (sa.size === 0 && sb.size === 0)
15
+ return 1;
16
+ let inter = 0;
17
+ for (const t of sa)
18
+ if (sb.has(t))
19
+ inter += 1;
20
+ const union = sa.size + sb.size - inter;
21
+ return union === 0 ? 1 : inter / union;
22
+ }
23
+ /**
24
+ * Similarity of two output texts, 0..1 (Jaccard over their tokenized bags). When either side tokenizes
25
+ * to nothing (e.g. all stopwords/punctuation), tokens carry no signal — fall back to exact normalized
26
+ * text equality so two DIFFERENT all-stopword outputs are not reported as identical.
27
+ */
28
+ export function similarity(a, b) {
29
+ const ta = tokenize(a);
30
+ const tb = tokenize(b);
31
+ if (ta.length === 0 || tb.length === 0)
32
+ return normalizeText(a) === normalizeText(b) ? 1 : 0;
33
+ return jaccard(ta, tb);
34
+ }
35
+ /** Lowercase, collapse runs of whitespace, and trim — the fallback comparison when there are no tokens. */
36
+ function normalizeText(s) {
37
+ return s.toLowerCase().replace(/\s+/g, ' ').trim();
38
+ }
39
+ /** Deterministic key-sorted JSON stringification (so field order never affects comparison). */
40
+ export function stableStringify(value) {
41
+ return JSON.stringify(sortValue(value));
42
+ }
43
+ function sortValue(value) {
44
+ if (Array.isArray(value))
45
+ return value.map(sortValue);
46
+ if (value && typeof value === 'object') {
47
+ const out = {};
48
+ for (const key of Object.keys(value).sort())
49
+ out[key] = sortValue(value[key]);
50
+ return out;
51
+ }
52
+ return value;
53
+ }
54
+ /** The text used to compare a candidate: its text, or a stable stringification of its JSON. */
55
+ export function comparableText(c) {
56
+ if (c.text !== undefined && c.text !== '')
57
+ return c.text;
58
+ if (c.json !== undefined)
59
+ return stableStringify(c.json);
60
+ return '';
61
+ }
62
+ /** Candidates that both ran successfully and produced comparable output. */
63
+ export function comparable(candidates) {
64
+ return candidates.filter((c) => c.ok && comparableText(c) !== '');
65
+ }
66
+ /**
67
+ * Greedy agreement clustering: each candidate joins the first cluster whose representative it is at
68
+ * least `threshold`-similar to, else starts its own. Deterministic in input order.
69
+ */
70
+ export function clusterByAgreement(candidates, threshold) {
71
+ const clusters = [];
72
+ for (const c of candidates) {
73
+ const text = comparableText(c);
74
+ const hit = clusters.find((cl) => similarity(cl.repText, text) >= threshold);
75
+ if (hit)
76
+ hit.labels.push(c.label);
77
+ else
78
+ clusters.push({ rep: c.label, repText: text, labels: [c.label] });
79
+ }
80
+ return clusters.map((cl) => ({ labels: cl.labels, representative: cl.rep }));
81
+ }
82
+ /** Every pair of candidates whose outputs are below the agreement threshold, most-different first. */
83
+ export function pairwiseDifferences(candidates, threshold) {
84
+ const out = [];
85
+ for (let i = 0; i < candidates.length; i += 1) {
86
+ for (let j = i + 1; j < candidates.length; j += 1) {
87
+ const sim = similarity(comparableText(candidates[i]), comparableText(candidates[j]));
88
+ if (sim < threshold)
89
+ out.push({ a: candidates[i].label, b: candidates[j].label, similarity: round(sim) });
90
+ }
91
+ }
92
+ return out.sort((x, y) => x.similarity - y.similarity);
93
+ }
94
+ /** Split text into trimmed sentence-ish units on sentence terminators and newlines. */
95
+ export function splitSentences(text) {
96
+ return text
97
+ .split(/(?<=[.!?])\s+|\n+/)
98
+ .map((s) => s.trim())
99
+ .filter((s) => s.length > 0);
100
+ }
101
+ /**
102
+ * For each candidate, sentences that no OTHER candidate expresses (similarity < 0.6 to every sentence of
103
+ * every other candidate). This surfaces information one model included and others omitted. Skipped when
104
+ * fewer than two candidates. Capped to keep output readable.
105
+ */
106
+ export function missingPoints(candidates, perCandidateCap = 5) {
107
+ if (candidates.length < 2)
108
+ return [];
109
+ const sentences = candidates.map((c) => splitSentences(comparableText(c)).map((s) => ({ text: s, tokens: tokenize(s) })));
110
+ const out = [];
111
+ for (let i = 0; i < candidates.length; i += 1) {
112
+ const others = sentences.filter((_, k) => k !== i).flat();
113
+ const unique = [];
114
+ for (const s of sentences[i]) {
115
+ if (s.tokens.length === 0)
116
+ continue;
117
+ const echoed = others.some((o) => jaccard(s.tokens, o.tokens) >= 0.6);
118
+ if (!echoed)
119
+ unique.push(s.text);
120
+ if (unique.length >= perCandidateCap)
121
+ break;
122
+ }
123
+ if (unique.length)
124
+ out.push({ label: candidates[i].label, points: unique });
125
+ }
126
+ return out;
127
+ }
128
+ /**
129
+ * Direct contradictions across STRUCTURED outputs only: for each top-level scalar field present in ≥2
130
+ * candidates' JSON, if the candidates disagree on its value it is a contradiction. Objects/arrays are not
131
+ * asserted as contradictions here (that needs semantic judgment, not a deterministic diff).
132
+ */
133
+ export function structuredContradictions(candidates) {
134
+ const byField = new Map();
135
+ for (const c of candidates) {
136
+ if (!c.json || typeof c.json !== 'object' || Array.isArray(c.json))
137
+ continue;
138
+ for (const [key, raw] of Object.entries(c.json)) {
139
+ if (!isScalar(raw))
140
+ continue;
141
+ const list = byField.get(key) ?? [];
142
+ list.push({ label: c.label, value: String(raw) });
143
+ byField.set(key, list);
144
+ }
145
+ }
146
+ const out = [];
147
+ for (const [field, values] of byField) {
148
+ if (values.length < 2)
149
+ continue;
150
+ const distinct = new Set(values.map((v) => v.value));
151
+ if (distinct.size >= 2)
152
+ out.push({ field, values });
153
+ }
154
+ return out.sort((a, b) => a.field.localeCompare(b.field));
155
+ }
156
+ function isScalar(v) {
157
+ return typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';
158
+ }
159
+ function round(n) {
160
+ return Math.round(n * 100) / 100;
161
+ }
162
+ /** Full deterministic analysis of a set of candidates at the given agreement threshold. */
163
+ export function analyze(candidates, threshold) {
164
+ const usable = comparable(candidates);
165
+ const clusters = clusterByAgreement(usable, threshold);
166
+ const differences = pairwiseDifferences(usable, threshold);
167
+ return {
168
+ clusters,
169
+ // Unanimity means EVERY pair agrees. Deriving it from pairwise differences (not cluster count) keeps
170
+ // it self-consistent: greedy single-link clustering is non-transitive, so `clusters.length === 1`
171
+ // could otherwise be true while `differences` is non-empty.
172
+ unanimous: usable.length > 0 && differences.length === 0,
173
+ differences,
174
+ missing: missingPoints(usable),
175
+ contradictions: structuredContradictions(usable),
176
+ };
177
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Comparator — the fan-out engine. It sends the SAME task to each pin through the ONE router
3
+ * (`AI.run()` with a provider/model pin), collects the outputs, analyzes them deterministically, and
4
+ * ranks them EVIDENCE-first:
5
+ * • evidence — a caller `validate()` scores each output objectively (e.g. run the tests); the passing
6
+ * one wins. This is authoritative and beats any judge, even when a judge is also configured.
7
+ * • judge — a labeled fallback: a pinned judge model picks a winner. Recorded as OPINION.
8
+ * • none — neither available: report agreement/differences only, no winner claimed.
9
+ * Budgets are obeyed: `maxCalls` caps total model calls (fan-out + judge), `maxParallelSteps` bounds
10
+ * concurrency, and `maxCostUsd` is passed to each run so no single candidate overspends. The comparator
11
+ * mutates nothing itself; an evidence `validate()` that runs tools does so through the caller's
12
+ * permission-gated runtime.
13
+ */
14
+ import type { AI } from '../plugin/ai.js';
15
+ import type { RoutingPreferences, RunRequest } from '../types.js';
16
+ import type { ComparisonCandidate, ComparisonPin, ComparisonResult, EvidenceResult } from './comparison.js';
17
+ export interface CompareInput {
18
+ /** Human description of what is being compared (for rendering/telemetry). */
19
+ goal: string;
20
+ /** Router task id. Defaults to 'chat'. */
21
+ task?: string;
22
+ /** The input sent to every pin. */
23
+ input: RunRequest['input'];
24
+ system?: string;
25
+ /** Output format for every pin (e.g. `{ format: 'json' }` to compare structured outputs). */
26
+ output?: RunRequest['output'];
27
+ /** The candidates to compare — at least two. */
28
+ pins: ComparisonPin[];
29
+ ai: AI;
30
+ /** Budget/concurrency (read from ExecutionPolicy by the Runtime). */
31
+ policy?: {
32
+ maxCalls?: number;
33
+ maxCostUsd?: number;
34
+ maxParallelSteps?: number;
35
+ };
36
+ /** Evidence function: objective validation of a candidate's output. Beats any judge. */
37
+ validate?: (candidate: ComparisonCandidate) => Promise<EvidenceResult> | EvidenceResult;
38
+ /** Optional judge model (a labeled fallback used only when no evidence is available). */
39
+ judge?: ComparisonPin;
40
+ /** User exclude/prefer routing applied to every pin (and the judge) — a hard-excluded model won't run. */
41
+ routing?: RoutingPreferences;
42
+ /** Agreement/clustering similarity threshold, 0..1 (default 0.75). */
43
+ agreementThreshold?: number;
44
+ signal?: AbortSignal;
45
+ }
46
+ export declare function compare(input: CompareInput): Promise<ComparisonResult>;
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Comparator — the fan-out engine. It sends the SAME task to each pin through the ONE router
3
+ * (`AI.run()` with a provider/model pin), collects the outputs, analyzes them deterministically, and
4
+ * ranks them EVIDENCE-first:
5
+ * • evidence — a caller `validate()` scores each output objectively (e.g. run the tests); the passing
6
+ * one wins. This is authoritative and beats any judge, even when a judge is also configured.
7
+ * • judge — a labeled fallback: a pinned judge model picks a winner. Recorded as OPINION.
8
+ * • none — neither available: report agreement/differences only, no winner claimed.
9
+ * Budgets are obeyed: `maxCalls` caps total model calls (fan-out + judge), `maxParallelSteps` bounds
10
+ * concurrency, and `maxCostUsd` is passed to each run so no single candidate overspends. The comparator
11
+ * mutates nothing itself; an evidence `validate()` that runs tools does so through the caller's
12
+ * permission-gated runtime.
13
+ */
14
+ import { analyze } from './analysis.js';
15
+ import { comparable, comparableText } from './analysis.js';
16
+ const DEFAULT_THRESHOLD = 0.75;
17
+ function pinLabel(pin, index) {
18
+ if (pin.label)
19
+ return pin.label;
20
+ if (pin.provider)
21
+ return pin.model ? `${pin.provider}/${pin.model}` : pin.provider;
22
+ return `candidate-${index + 1}`;
23
+ }
24
+ /**
25
+ * Assign genuinely-unique display labels, preserving order. A generated `base#n` is re-checked against
26
+ * every label already emitted, so it can never collide with a caller-supplied label like "a#2".
27
+ */
28
+ function uniqueLabels(pins) {
29
+ const used = new Set();
30
+ return pins.map((pin, i) => {
31
+ const base = pinLabel(pin, i);
32
+ let label = base;
33
+ let n = 2;
34
+ while (used.has(label))
35
+ label = `${base}#${n++}`;
36
+ used.add(label);
37
+ return label;
38
+ });
39
+ }
40
+ /** Run `items` through `fn` in order, at most `limit` concurrently, aborting further batches on signal. */
41
+ async function boundedMap(items, limit, fn, signal) {
42
+ const max = Math.max(1, limit);
43
+ const out = [];
44
+ for (let i = 0; i < items.length; i += max) {
45
+ if (signal?.aborted)
46
+ break;
47
+ const batch = items.slice(i, i + max);
48
+ const results = await Promise.all(batch.map((item, j) => fn(item, i + j)));
49
+ out.push(...results);
50
+ }
51
+ return out;
52
+ }
53
+ function latencyOf(run) {
54
+ return run.routing.attempts.reduce((sum, a) => sum + (a.latencyMs ?? 0), 0);
55
+ }
56
+ /** The provider/model of the router's last attempt — used to attribute a failed run that selected nothing. */
57
+ function lastAttempt(run) {
58
+ const a = run.routing.attempts[run.routing.attempts.length - 1];
59
+ return a ? { providerId: a.providerId, model: a.model } : undefined;
60
+ }
61
+ export async function compare(input) {
62
+ const task = input.task ?? 'chat';
63
+ const threshold = input.agreementThreshold ?? DEFAULT_THRESHOLD;
64
+ if (input.pins.length < 2)
65
+ throw new Error('comparison requires at least two pins');
66
+ const labels = uniqueLabels(input.pins);
67
+ const maxCalls = input.policy?.maxCalls;
68
+ const parallel = input.policy?.maxParallelSteps ?? 2;
69
+ // Budget: maxCalls caps TOTAL model calls (fan-out pins + an optional judge). Reserve one call for the
70
+ // judge when one is configured and no evidence function will decide the ranking.
71
+ const judgeReserve = input.judge && !input.validate ? 1 : 0;
72
+ const pinBudget = maxCalls === undefined ? input.pins.length : Math.max(0, maxCalls - judgeReserve);
73
+ const toRun = Math.min(input.pins.length, pinBudget);
74
+ const budgetSkipped = input.pins.length - toRun;
75
+ const runPins = input.pins.slice(0, toRun);
76
+ const runLabels = labels.slice(0, toRun);
77
+ const candidates = await boundedMap(runPins, parallel, async (pin, index) => {
78
+ const req = {
79
+ task,
80
+ input: input.input,
81
+ ...(input.system ? { system: input.system } : {}),
82
+ ...(input.output ? { output: input.output } : {}),
83
+ ...(input.routing ? { routing: input.routing } : {}),
84
+ ...(pin.provider ? { provider: pin.provider } : {}),
85
+ ...(pin.model ? { model: pin.model } : {}),
86
+ ...(input.policy?.maxCostUsd !== undefined ? { budget: { maxCostUsd: input.policy.maxCostUsd } } : {}),
87
+ };
88
+ let run;
89
+ try {
90
+ run = await input.ai.run(req);
91
+ }
92
+ catch (err) {
93
+ // The router normalizes provider failures into ok:false results; an unexpected throw must not
94
+ // crash the whole fan-out — record it as a failed candidate instead.
95
+ return { label: runLabels[index], pin, ok: false, error: { category: 'UNKNOWN', message: err instanceof Error ? err.message : String(err), retryable: false } };
96
+ }
97
+ const attempted = run.routing.selected ?? lastAttempt(run);
98
+ return {
99
+ label: runLabels[index],
100
+ pin,
101
+ ok: run.ok,
102
+ ...(run.response?.text !== undefined ? { text: run.response.text } : {}),
103
+ ...(run.response?.json !== undefined ? { json: run.response.json } : {}),
104
+ ...(run.routing.selected ? { selected: run.routing.selected } : {}),
105
+ ...(attempted ? { attempted } : {}),
106
+ confidence: run.confidence,
107
+ latencyMs: latencyOf(run),
108
+ ...(run.error ? { error: run.error } : {}),
109
+ };
110
+ }, input.signal);
111
+ const usable = comparable(candidates);
112
+ // Evidence pass (authoritative). Runs the caller's objective check on each successful candidate.
113
+ if (input.validate) {
114
+ await boundedMap(usable, parallel, async (c) => {
115
+ c.evidence = await input.validate(c);
116
+ return c;
117
+ }, input.signal);
118
+ }
119
+ const analysis = analyze(candidates, threshold);
120
+ const ranking = await rank(usable, task, input);
121
+ const signals = buildSignals(candidates, task, ranking);
122
+ // Accounting: `ran` is what actually completed; `skipped` = requested − ran covers BOTH budget-skipped
123
+ // pins and any pins dropped by an abort (so requested === ran + skipped always holds).
124
+ const aborted = (input.signal?.aborted ?? false) || candidates.length < toRun;
125
+ const usage = {
126
+ requested: input.pins.length,
127
+ ran: candidates.length,
128
+ skipped: input.pins.length - candidates.length,
129
+ ...(aborted ? { aborted: true } : {}),
130
+ ...(aborted
131
+ ? { note: `aborted before all pins ran (${candidates.length}/${input.pins.length})` }
132
+ : budgetSkipped > 0
133
+ ? { note: `skipped ${budgetSkipped} pin(s): call budget (maxCalls=${maxCalls}) reached` }
134
+ : {}),
135
+ };
136
+ return { goal: input.goal, task, candidates, analysis, ranking, usage, signals };
137
+ }
138
+ async function rank(usable, task, input) {
139
+ // 1) Evidence — authoritative. Ordered by (passed, score, confidence). Winner only if one passed. Only
140
+ // taken when evidence was actually gathered for EVERY usable candidate — a run cut short by an abort
141
+ // must not be reported as an authoritative evidence verdict.
142
+ if (input.validate && usable.length > 0 && usable.every((c) => c.evidence !== undefined)) {
143
+ const ranked = [...usable].sort(byEvidence);
144
+ const winner = ranked.find((c) => c.evidence?.ok);
145
+ return {
146
+ method: 'evidence',
147
+ judgeOpinion: false,
148
+ ranked: ranked.map((c) => ({ label: c.label, score: c.evidence?.score ?? 0, why: evidenceWhy(c) })),
149
+ ...(winner ? { winner: winner.label } : {}),
150
+ };
151
+ }
152
+ // 2) Judge — labeled opinion. Requires at least two candidates to actually compare (a verdict over a
153
+ // lone survivor is vacuous). The judge runs as one extra model call reserved from the budget.
154
+ if (input.judge && usable.length >= 2) {
155
+ const verdict = await runJudge(usable, task, input);
156
+ if (verdict) {
157
+ const ordered = [verdict.winner, ...usable.map((c) => c.label).filter((l) => l !== verdict.winner)];
158
+ const ranked = ordered.map((label, i) => ({
159
+ label,
160
+ score: i === 0 ? 1 : 0,
161
+ why: i === 0 ? `judge picked this (opinion, not evidence): ${verdict.reason}` : 'not selected by judge',
162
+ }));
163
+ return { method: 'judge', judgeOpinion: true, ranked, winner: verdict.winner };
164
+ }
165
+ }
166
+ // 3) None — report only. Ordered by router confidence, but NO winner is claimed (not a verdict).
167
+ const ranked = [...usable].sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
168
+ return {
169
+ method: 'none',
170
+ judgeOpinion: false,
171
+ ranked: ranked.map((c) => ({ label: c.label, score: round(c.confidence ?? 0), why: 'no evidence or judge — ordered by router-reported confidence (not a quality verdict)' })),
172
+ };
173
+ }
174
+ function byEvidence(a, b) {
175
+ const ao = a.evidence?.ok ? 1 : 0;
176
+ const bo = b.evidence?.ok ? 1 : 0;
177
+ if (ao !== bo)
178
+ return bo - ao;
179
+ const as = a.evidence?.score ?? 0;
180
+ const bs = b.evidence?.score ?? 0;
181
+ if (as !== bs)
182
+ return bs - as;
183
+ return (b.confidence ?? 0) - (a.confidence ?? 0);
184
+ }
185
+ function evidenceWhy(c) {
186
+ const e = c.evidence;
187
+ if (!e)
188
+ return 'no evidence gathered';
189
+ const passed = e.checks.filter((k) => k.ok).length;
190
+ const failed = e.checks.find((k) => !k.ok);
191
+ return e.ok
192
+ ? `evidence passed${e.checks.length ? ` (${passed}/${e.checks.length} checks)` : ''}`
193
+ : `evidence failed${e.checks.length ? ` (${passed}/${e.checks.length} checks${failed?.why ? `; ${failed.why}` : ''})` : ''}`;
194
+ }
195
+ /** Ask a pinned judge model to pick the best candidate. Returns undefined if it errors or votes for an unknown label. */
196
+ async function runJudge(usable, task, input) {
197
+ const judge = input.judge;
198
+ const listing = usable.map((c) => `[${c.label}]\n${truncate(comparableText(c), 1200)}`).join('\n\n');
199
+ const prompt = [
200
+ `Compare the following candidate answers to this goal and pick the single best one by correctness and completeness.`,
201
+ `Goal: ${input.goal}`,
202
+ ``,
203
+ `Candidates:`,
204
+ listing,
205
+ ``,
206
+ `Respond ONLY as JSON: {"winner": "<one of the candidate labels above>", "reason": "<one sentence>"}.`,
207
+ ].join('\n');
208
+ const req = {
209
+ task,
210
+ input: { text: prompt },
211
+ output: { format: 'json' },
212
+ ...(input.routing ? { routing: input.routing } : {}),
213
+ ...(judge.provider ? { provider: judge.provider } : {}),
214
+ ...(judge.model ? { model: judge.model } : {}),
215
+ ...(input.policy?.maxCostUsd !== undefined ? { budget: { maxCostUsd: input.policy.maxCostUsd } } : {}),
216
+ };
217
+ let run;
218
+ try {
219
+ run = await input.ai.run(req);
220
+ }
221
+ catch {
222
+ return undefined;
223
+ }
224
+ // The judge output is untrusted DATA: it may only NAME a candidate label. A non-object (null, array,
225
+ // primitive) or malformed JSON is discarded, never dereferenced.
226
+ const j = run.response?.json;
227
+ if (!run.ok || typeof j !== 'object' || j === null || Array.isArray(j))
228
+ return undefined;
229
+ const parsed = j;
230
+ const winnerRaw = typeof parsed.winner === 'string' ? parsed.winner.trim() : '';
231
+ // The judge must name a real candidate; match case-insensitively, else discard the verdict.
232
+ const match = usable.find((c) => c.label.toLowerCase() === winnerRaw.toLowerCase());
233
+ if (!match)
234
+ return undefined;
235
+ return { winner: match.label, reason: typeof parsed.reason === 'string' ? parsed.reason : 'no reason given' };
236
+ }
237
+ /**
238
+ * One learning signal per fanned-out candidate (Phase 9 consumes these). `preferred` is set ONLY when
239
+ * EVIDENCE established the winner — never a judge or confidence — so preference learning is evidence-gated.
240
+ */
241
+ function buildSignals(candidates, task, ranking) {
242
+ const out = [];
243
+ for (const c of candidates) {
244
+ // Attribution prefers what actually ran (selected), then what the router attempted (so a FAILED
245
+ // model-less pin still yields a negative signal), then the pin itself.
246
+ const providerId = c.selected?.providerId ?? c.attempted?.providerId ?? c.pin.provider;
247
+ const model = c.selected?.model ?? c.attempted?.model ?? c.pin.model;
248
+ if (!providerId || !model)
249
+ continue; // cannot attribute a signal without a concrete provider+model
250
+ const preferred = ranking.method === 'evidence' && ranking.winner === c.label && c.evidence?.ok === true;
251
+ out.push({
252
+ providerId,
253
+ model,
254
+ task,
255
+ ran: true,
256
+ ok: c.ok,
257
+ ...(c.ok && c.confidence !== undefined ? { confidence: c.confidence } : {}),
258
+ ...(c.latencyMs !== undefined ? { latencyMs: c.latencyMs } : {}),
259
+ ...(c.evidence ? { evidenceOk: c.evidence.ok, evidenceScore: c.evidence.score } : {}),
260
+ preferred,
261
+ });
262
+ }
263
+ return out;
264
+ }
265
+ function truncate(s, n) {
266
+ return s.length > n ? s.slice(0, n) + '…' : s;
267
+ }
268
+ function round(n) {
269
+ return Math.round(n * 100) / 100;
270
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Comparison types (Phase 8). A comparison fans out the SAME task across several **pins** (candidate
3
+ * provider/model pairs), collects the outputs, and analyzes them: agreement, differences,
4
+ * contradictions, missing information. Ranking is EVIDENCE-first — a caller-supplied validation of each
5
+ * output (e.g. "run the tests") beats any judge model's opinion; a judge is a labeled fallback, and with
6
+ * neither we report only (no verdict). Pure types — no router/AI import, so `runtime/types.ts` can widen
7
+ * its `comparison` placeholder to `ComparisonResult` without an import cycle.
8
+ */
9
+ import type { RouterErrorInfo } from '../types.js';
10
+ /** A candidate to compare: a pinned provider (and optionally model). Passes through all router filters. */
11
+ export interface ComparisonPin {
12
+ /** Provider id to pin. Omit to let the router choose (rarely useful for a comparison). */
13
+ provider?: string;
14
+ /** Model id to pin (optional; the provider's default model when omitted). */
15
+ model?: string;
16
+ /** Display label; defaults to `provider/model` (or `provider`, or `candidate-N`). */
17
+ label?: string;
18
+ }
19
+ /** Objective, evidence-based validation of a single candidate's output. Beats judge opinion. */
20
+ export interface EvidenceResult {
21
+ ok: boolean;
22
+ /** Fraction of checks that passed, 0..1 (1/0 when there are no discrete checks). */
23
+ score: number;
24
+ checks: Array<{
25
+ name: string;
26
+ ok: boolean;
27
+ why?: string;
28
+ }>;
29
+ }
30
+ /** One fanned-out result: what a single pin produced, plus any evidence gathered for it. */
31
+ export interface ComparisonCandidate {
32
+ label: string;
33
+ pin: ComparisonPin;
34
+ /** Whether the underlying `AI.run()` for this pin succeeded. */
35
+ ok: boolean;
36
+ text?: string;
37
+ json?: unknown;
38
+ /** What the router actually selected under this pin (may differ when the model was left open). */
39
+ selected?: {
40
+ providerId: string;
41
+ model: string;
42
+ };
43
+ /** The provider/model the router last ATTEMPTED — for attribution when a run failed with nothing selected. */
44
+ attempted?: {
45
+ providerId: string;
46
+ model: string;
47
+ };
48
+ /** Router-reported confidence for this candidate's run (an estimate, never a quality verdict alone). */
49
+ confidence?: number;
50
+ /** Total latency across the run's attempts (primary + any fallbacks/retries). */
51
+ latencyMs?: number;
52
+ error?: RouterErrorInfo;
53
+ /** Present only when a `validate()` evidence function ran for this candidate. */
54
+ evidence?: EvidenceResult;
55
+ }
56
+ /** How the ranking was decided. `evidence` is authoritative; `judge` is opinion; `none` is report-only. */
57
+ export type ComparisonMethod = 'evidence' | 'judge' | 'none';
58
+ /** A set of candidates whose outputs agree (by normalized token similarity). */
59
+ export interface AgreementCluster {
60
+ labels: string[];
61
+ representative: string;
62
+ }
63
+ export interface ComparisonDifference {
64
+ a: string;
65
+ b: string;
66
+ /** Token-set Jaccard similarity of the two outputs, 0..1. */
67
+ similarity: number;
68
+ }
69
+ /** Content one candidate surfaced that the others did not (missing-information). */
70
+ export interface MissingPoint {
71
+ label: string;
72
+ points: string[];
73
+ }
74
+ /** A direct contradiction: the same structured field carrying different values across candidates. */
75
+ export interface Contradiction {
76
+ field: string;
77
+ values: Array<{
78
+ label: string;
79
+ value: string;
80
+ }>;
81
+ }
82
+ export interface ComparisonAnalysis {
83
+ clusters: AgreementCluster[];
84
+ /** True when every successful candidate agreed (a single cluster). */
85
+ unanimous: boolean;
86
+ differences: ComparisonDifference[];
87
+ missing: MissingPoint[];
88
+ contradictions: Contradiction[];
89
+ }
90
+ export interface RankEntry {
91
+ label: string;
92
+ score: number;
93
+ why: string;
94
+ }
95
+ export interface ComparisonRanking {
96
+ method: ComparisonMethod;
97
+ /** True when the verdict is a judge model's OPINION, not objective evidence. */
98
+ judgeOpinion: boolean;
99
+ ranked: RankEntry[];
100
+ /** Set only for an evidence-grounded (or judge) winner; absent for report-only comparisons. */
101
+ winner?: string;
102
+ }
103
+ /**
104
+ * A learning signal emitted per fanned-out candidate for the Phase 9 learning stores to consume.
105
+ * `preferred` is set ONLY when EVIDENCE (never a judge or router confidence) established the winner —
106
+ * so preference learning can never be driven by opinion. Failed runs are emitted too (`ran:true`,
107
+ * `ok:false`) as negative signal.
108
+ */
109
+ export interface ComparisonSignal {
110
+ providerId: string;
111
+ model: string;
112
+ task: string;
113
+ ran: boolean;
114
+ ok: boolean;
115
+ confidence?: number;
116
+ latencyMs?: number;
117
+ evidenceOk?: boolean;
118
+ evidenceScore?: number;
119
+ preferred: boolean;
120
+ }
121
+ export interface ComparisonUsage {
122
+ /** Pins the caller asked to compare. */
123
+ requested: number;
124
+ /** Pins actually run (bounded by the call budget). */
125
+ ran: number;
126
+ /** Pins that did not run — budget-skipped and/or dropped by an abort (= requested − ran). */
127
+ skipped: number;
128
+ /** True when the fan-out was aborted before every intended pin ran (the result is partial). */
129
+ aborted?: boolean;
130
+ note?: string;
131
+ }
132
+ export interface ComparisonResult {
133
+ goal: string;
134
+ task: string;
135
+ candidates: ComparisonCandidate[];
136
+ analysis: ComparisonAnalysis;
137
+ ranking: ComparisonRanking;
138
+ usage: ComparisonUsage;
139
+ signals: ComparisonSignal[];
140
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Comparison types (Phase 8). A comparison fans out the SAME task across several **pins** (candidate
3
+ * provider/model pairs), collects the outputs, and analyzes them: agreement, differences,
4
+ * contradictions, missing information. Ranking is EVIDENCE-first — a caller-supplied validation of each
5
+ * output (e.g. "run the tests") beats any judge model's opinion; a judge is a labeled fallback, and with
6
+ * neither we report only (no verdict). Pure types — no router/AI import, so `runtime/types.ts` can widen
7
+ * its `comparison` placeholder to `ComparisonResult` without an import cycle.
8
+ */
9
+ export {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Human-readable rendering of a ComparisonResult for the terminal (REPL + one-shot). Pure: turns the
3
+ * structured result into lines. It never claims a verdict the ranking did not make — a judge verdict is
4
+ * labeled as opinion, and a report-only comparison shows no winner.
5
+ */
6
+ import type { ComparisonResult } from './comparison.js';
7
+ export declare function renderComparison(result: ComparisonResult): string[];