openmerit 0.1.12 → 0.2.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 (56) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +5 -8
  3. package/dist/pi/scout-extension.mjs +23 -10
  4. package/dist/terminal/public/app.js +37 -37
  5. package/dist/terminal/public/styles.css +0 -125
  6. package/dist/terminal/scout-update.json +1 -1
  7. package/dist/terminal/src/cli.mjs +2 -0
  8. package/dist/terminal/src/install.mjs +1 -0
  9. package/dist/terminal/src/scout-instructions.d.ts +2 -2
  10. package/dist/terminal/src/scout-instructions.js +23 -10
  11. package/dist/terminal/src/scout-instructions.ts +23 -10
  12. package/dist/terminal/src/scout.d.ts +77 -0
  13. package/dist/terminal/src/scout.js +181 -0
  14. package/dist/terminal/src/scout.ts +164 -0
  15. package/dist/terminal/src/server.mjs +1 -1
  16. package/docs/commands.md +2 -78
  17. package/docs/getting-started.md +2 -2
  18. package/docs/index.md +4 -13
  19. package/docs/inference-terminal.md +2 -63
  20. package/docs/pi-extension.md +1 -1
  21. package/docs/roadmap.md +4 -17
  22. package/docs/scout.md +29 -250
  23. package/docs/security.md +5 -17
  24. package/docs/testing.md +7 -35
  25. package/docs/troubleshooting.md +1 -1
  26. package/package.json +4 -24
  27. package/dist/core/src/index.d.ts +0 -101
  28. package/dist/core/src/index.js +0 -1649
  29. package/dist/core/src/store.d.ts +0 -35
  30. package/dist/core/src/store.js +0 -102
  31. package/dist/protocol/src/index.d.ts +0 -484
  32. package/dist/protocol/src/index.js +0 -47
  33. package/dist/protocol/src/schemas.d.ts +0 -576
  34. package/dist/protocol/src/schemas.js +0 -280
  35. package/dist/terminal/public/brands/vllm.ico +0 -0
  36. package/dist/terminal/public/flow.css +0 -1
  37. package/dist/terminal/public/flow.js +0 -770
  38. package/dist/terminal/src/demo-policy.mjs +0 -205
  39. package/dist/terminal/src/enterprise-sample.mjs +0 -151
  40. package/docs/adapter-guide.md +0 -191
  41. package/docs/architecture.md +0 -61
  42. package/docs/automation.md +0 -76
  43. package/docs/budgets.md +0 -39
  44. package/docs/demo-backfill.md +0 -31
  45. package/docs/demo-fieldkit.md +0 -49
  46. package/docs/demo-placement.md +0 -32
  47. package/docs/demo-spam.md +0 -17
  48. package/docs/demo-support.md +0 -44
  49. package/docs/demo.md +0 -74
  50. package/docs/first-trial.md +0 -62
  51. package/docs/interactive-demo.md +0 -118
  52. package/docs/lifecycle.md +0 -32
  53. package/docs/metrics-and-evidence.md +0 -48
  54. package/docs/operations.md +0 -40
  55. package/docs/pareto-spec.md +0 -76
  56. package/docs/ux-reference.md +0 -32
@@ -1,205 +0,0 @@
1
- // A reviewed model/service-offer catalog for the hosted fictional comparison only.
2
- // Scores are publisher-reported point estimates, not insurance-workload evaluations.
3
- export const EVIDENCE_DATE = '2026-10-10';
4
- export const PREFERENCES = [
5
- { value: 'default', label: 'Default · within your stack', note: 'Search all configured services; choose the lowest modeled cost among qualified replacements.' },
6
- { value: 'zdr', label: 'Bias ZDR models', note: 'Prefer confirmed ZDR endpoints. Eligibility alone does not establish account approval or enabled settings.' },
7
- { value: 'open', label: 'Bias open-weights models', note: 'Prefer models with open weights.' },
8
- { value: 'closed', label: 'No open-weights models', note: 'Require a closed-weight assignment for every workload.' },
9
- { value: 'usa', label: 'Bias USA models', note: 'Prefer models from US developers. This does not specify hosting location or data residency.' },
10
- { value: 'china', label: 'Bias Chinese models', note: 'Prefer models from Chinese developers. This does not specify hosting location or data residency.' },
11
- ];
12
- export const preferenceFor = value => PREFERENCES.find(item => item.value === value) ?? PREFERENCES[0];
13
- const oa = 'https://openai.com/index/gpt-4-1/';
14
- const claude = 'https://www-cdn.anthropic.com/bbd8ef16d70b7a1665f14f306ee88b53f686aa75/Claude%20Sonnet%204.6%20System%20Card.pdf';
15
- const gemini = 'https://storage.googleapis.com/deepmind-media/gemini/gemini_3_flash_model_evaluation.pdf';
16
- const metric = (axis, name, value, suite, source, settings) => ({ axis, name, value, suite, source, settings, unit: '%', direction: 'higher' });
17
- const oaScores = (tool, capability, reasoning) => [
18
- metric('tools', 'τ-bench retail', tool, 'openai-4.1-tau-retail-gpt4o-user', oa, '5-run average; original GPT-4o user simulator; no custom tools or prompting.'),
19
- metric('capabilities', 'MMLU', capability, 'openai-4.1-mmlu', oa, 'OpenAI GPT-4.1 release comparison; GPT-4o snapshot 2024-11-20.'),
20
- metric('reasoning', 'AIME 2024', reasoning, 'openai-4.1-aime24', oa, 'OpenAI GPT-4.1 release comparison, AIME 2024.'),
21
- ];
22
- const claudeScores = (tool, capability, reasoning) => [
23
- metric('tools', 'τ²-bench retail', tool, 'anthropic-sonnet46-tau2-retail', claude, 'Sonnet 4.6 system card §2.5; max effort, 10 trials for Sonnet; published Opus configuration.'),
24
- metric('capabilities', 'MMMU-Pro · no tools', capability, 'anthropic-sonnet46-mmmupro-notools', claude, 'Sonnet 4.6 system card Table 2.1.A; publisher model configurations.'),
25
- metric('reasoning', 'GPQA Diamond', reasoning, 'anthropic-sonnet46-gpqa', claude, 'Sonnet 4.6 system card §2.8; adaptive thinking, max effort for Sonnet; published Opus configuration.'),
26
- ];
27
- const googleScores = (tool, capability, reasoning) => [
28
- metric('tools', 'τ²-bench', tool, 'google-3flash-tau2-fixed-airline', gemini, 'Gemini 3 Pro user simulator; fixed Airline domain; mean of Retail, Airline and Telecom.'),
29
- metric('capabilities', 'MMMU-Pro', capability, 'google-3flash-mmmupro', gemini, 'Thinking; mean of Standard and Vision settings; no tools.'),
30
- metric('reasoning', 'GPQA Diamond', reasoning, 'google-3flash-gpqa', gemini, 'Thinking; no tools; Google December 2025 comparison.'),
31
- ];
32
- const ossCard = 'https://arxiv.org/html/2508.10925v1';
33
- const ossScores = (tool, capability, reasoning) => [
34
- metric('tools', 'τ-bench retail', tool, 'openai-oss-high-retail', ossCard, 'Table 3; high reasoning effort; developer function calling.'),
35
- metric('capabilities', 'MMLU', capability, 'openai-oss-high-mmlu', ossCard, 'Table 3; high reasoning effort, default system prompt, pass@1.'),
36
- metric('reasoning', 'GPQA Diamond · no tools', reasoning, 'openai-oss-high-gpqa', ossCard, 'Table 3; high reasoning effort, no tools, pass@1.'),
37
- ];
38
- const dsCard = 'https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash';
39
- const dsScores = (tool, capability, reasoning) => [
40
- metric('tools', 'AutomationBench', tool, 'deepseek41-automation', dsCard, 'Frontier instruct-model comparison; official scaffold, maximum reasoning effort, temperature 1, top-p .95.'),
41
- metric('capabilities', "Agent’s Last Exam", capability, 'deepseek41-ale', dsCard, 'Frontier instruct-model comparison; broad agent tasks, official scaffold, maximum reasoning effort.'),
42
- metric('reasoning', 'GPQA Diamond', reasoning, 'deepseek41-gpqa', dsCard, 'Frontier instruct-model comparison; pass@1, maximum reasoning effort. Published model scores, not endpoint measurements.'),
43
- ];
44
- const qwenCard = 'https://huggingface.co/Qwen/Qwen3.6-27B';
45
- const qwenScores = (tool, pass3, capability, reasoning) => [
46
- metric('tools', 'ClawEval · average', tool, 'qwen36-claw-avg', qwenCard, 'Qwen 3.6 27B release comparison; published model configurations.'),
47
- metric('tools', 'ClawEval · pass³', pass3, 'qwen36-claw-pass3', qwenCard, 'Same comparison, stricter repeated-task success. Both tool metrics must pass.'),
48
- metric('capabilities', 'MMMU-Pro', capability, 'qwen36-mmmupro', qwenCard, 'Vision benchmark; published Qwen 3.6 comparison configurations.'),
49
- metric('reasoning', 'GPQA Diamond', reasoning, 'qwen36-gpqa', qwenCard, 'Published Qwen 3.6 comparison configurations.'),
50
- ];
51
- const cohortScores = (source, suite, names, values, settings) => ['tools', 'capabilities', 'reasoning'].map((axis, index) => metric(axis, names[index], values[index], `${suite}-${axis}`, source, settings));
52
- const flashCard = 'https://huggingface.co/zai-org/GLM-5.3-Flash';
53
- const flashScores = values => cohortScores(flashCard, 'glm53flash-release', ['AutomationBench v1.0.6', 'GDPval-AA v2', 'HLE · with tools'], values, 'Published release chart and footnotes; maximum effort; GDPval-AA rating, not percent.').map(item => item.axis === 'capabilities' ? { ...item, unit: ' rating' } : item);
54
- const qwen38Card = 'https://huggingface.co/Qwen/Qwen3.8-27B';
55
- const qwen38Scores = values => cohortScores(qwen38Card, 'qwen38-release', ['ClawEval-MM · average', 'IFBench', 'GPQA Diamond'], values, 'Qwen 3.8 model card comparison; published thinking configurations. ClawEval-MM is distinct from ClawEval.');
56
- // The application-code profile screens generated SQL/Python used by the
57
- // insurance product. These are application workers, not developer-harness usage.
58
- // Keep publisher cohorts separate: LiveCodeBench v6 % is not LCB Pro Elo.
59
- const glm47Card = 'https://huggingface.co/zai-org/GLM-4.7';
60
- const glmCodeScores = values => cohortScores(glm47Card, 'glm47-application-code', ['τ²-bench', 'LiveCodeBench v6', 'GPQA Diamond'], values, 'GLM-4.7 release table; thinking enabled, preserved thinking for multi-turn tools; adjusted user prompts and fixed Airline domain. Published comparator configurations; not endpoint parity.');
61
- const googleCodeScores = values => cohortScores(gemini, 'google-3flash-application-code', ['τ²-bench', 'LiveCodeBench Pro', 'GPQA Diamond'], values, 'December 2025 comparison: Gemini thinking, Sonnet 4.5 high/best available thinking. LCB Pro official leaderboard Elo; τ² fixed Airline, Gemini user simulator for Gemini and publisher-reported Sonnet score. Public comparison, not a controlled endpoint evaluation.').map(item => item.axis === 'capabilities' ? { ...item, unit: ' Elo' } : item);
62
- const basetenPolicy = { confirmedZdr: true, policySource: 'https://www.baseten.co/security-practices/', endpointMode: 'Synchronous Model API; request/response logging off' };
63
- const model = (id, name, provider, input, output, source, benchmarks = [], extra = {}) => ({ id, modelId: id, name, provider, input, output, source, benchmarks, creator: provider, origin: 'US', openWeights: false, outputType: 'text', confirmedZdr: false, ...extra });
64
- const openai = name => `https://developers.openai.com/api/docs/models/${name}`;
65
- const anthropic = 'https://platform.claude.com/docs/en/about-claude/pricing';
66
- const google = 'https://ai.google.dev/gemini-api/docs/pricing';
67
- const groq = 'https://console.groq.com/docs/models';
68
- const baseten = 'https://www.baseten.co/pricing/';
69
- const mistral = 'https://docs.mistral.ai/inference/pricing';
70
- export const CATALOG = [
71
- model('gpt4o', 'GPT-4o', 'OpenAI', 2.5, 10, openai('gpt-4o'), oaScores(60.3, 85.7, 13.1)),
72
- model('gpt41', 'GPT-4.1', 'OpenAI', 2, 8, openai('gpt-4.1'), oaScores(68, 90.2, 48.1)),
73
- model('gpt41mini', 'GPT-4.1 mini', 'OpenAI', .4, 1.6, openai('gpt-4.1-mini'), oaScores(55.8, 87.5, 49.6)),
74
- model('gpt54', 'GPT-5.4', 'OpenAI', 2.5, 15, openai('gpt-5.4')),
75
- model('gpt54mini', 'GPT-5.4 mini', 'OpenAI', .75, 4.5, openai('gpt-5.4-mini')),
76
- model('opus45', 'Claude Opus 4.5', 'Anthropic', 5, 25, anthropic, [...claudeScores(88.9, 70.6, 87), ...qwenScores(76.6, 59.6, 70.6, 87)]),
77
- model('sonnet46', 'Claude Sonnet 4.6', 'Anthropic', 3, 15, anthropic, claudeScores(91.7, 74.5, 89.9)),
78
- model('sonnet45', 'Claude Sonnet 4.5', 'Anthropic', 3, 15, anthropic, [...glmCodeScores([87.2, 64.0, 83.4]), ...googleCodeScores([87.2, 1418, 83.4])], { configuration: 'Thinking enabled; published comparison configurations' }),
79
- model('haiku45', 'Claude Haiku 4.5', 'Anthropic', 1, 5, anthropic),
80
- model('gemini25pro', 'Gemini 2.5 Pro', 'Google', 1.25, 10, google, googleScores(77.8, 68, 86.4)),
81
- model('gemini25flash', 'Gemini 2.5 Flash', 'Google', .3, 2.5, google, googleScores(79.5, 66.7, 82.8)),
82
- model('gemini3flash', 'Gemini 3 Flash Preview', 'Google', .5, 3, google, [...googleScores(90.2, 81.2, 90.4), ...googleCodeScores([90.2, 2316, 90.4])]),
83
- model('oss120', 'gpt-oss-120b', 'Groq', .15, .6, groq, ossScores(67.8, 90, 80.1), { creator: 'OpenAI', openWeights: true, configuration: 'High reasoning effort' }),
84
- model('oss20', 'gpt-oss-20b', 'Groq', .075, .3, groq, ossScores(54.8, 85.3, 71.5), { creator: 'OpenAI', openWeights: true, configuration: 'High reasoning effort' }),
85
- model('glm53', 'GLM-5.3', 'Baseten', 1.4, 4.4, baseten, dsScores(48.8, 28.5, 88.1), { creator: 'Z.ai', origin: 'CN', openWeights: true, ...basetenPolicy, configuration: 'Maximum reasoning effort' }),
86
- model('glm53flash', 'GLM-5.3-Flash', 'Baseten', .15, .5, baseten, flashScores([48.8, 1773, 55.3]), { creator: 'Z.ai', origin: 'CN', openWeights: true, ...basetenPolicy }),
87
- model('mistral35', 'Mistral Medium 3.5', 'Mistral', 1.5, 7.5, mistral, [], { origin: 'FR', openWeights: true, metadataSource: 'https://docs.mistral.ai/models/mistral-medium-3-5-26-04' }),
88
- model('mistral4', 'Mistral Small 4', 'Mistral', .15, .6, mistral, [], { origin: 'FR', openWeights: true }),
89
- model('oss120baseten', 'gpt-oss-120b', 'Baseten', .1, .5, baseten, ossScores(67.8, 90, 80.1), { modelId: 'oss120', creator: 'OpenAI', openWeights: true, ...basetenPolicy, configuration: 'High reasoning effort; same published checkpoint' }),
90
- model('deepseek41flash', 'DeepSeek V4.1 Flash', 'Baseten', .3, 1.2, baseten, dsScores(54.8, 31.8, 90.9), { creator: 'DeepSeek', origin: 'CN', openWeights: true, ...basetenPolicy, configuration: 'Maximum reasoning effort (100)' }),
91
- model('deepseek4pro0813', 'DeepSeek V4 Pro 0813', 'Baseten', 1.32, 3.96, baseten, [], { creator: 'DeepSeek', origin: 'CN', openWeights: true, ...basetenPolicy, metadataSource: 'https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro-0813' }),
92
- model('nemotron3ultra', 'Nemotron 3 Ultra', 'Baseten', .6, 2.4, baseten, cohortScores('https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16', 'nvidia-ultra-nemo', ['TauBench V3 · average', 'MMLU-Pro', 'GPQA · no tools'], [70.9, 86.8, 87.0], 'NVIDIA NeMo evaluation SDK; model-card configurations. TauBench V3 is not τ-bench or τ²-bench.'), { creator: 'NVIDIA', origin: 'US', openWeights: true, ...basetenPolicy, metadataSource: 'https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16' }),
93
- model('glm52', 'GLM-5.2', 'Baseten', 1.4, 4.4, baseten, flashScores([26.2, 1504, 54.7]), { creator: 'Z.ai', origin: 'CN', openWeights: true, ...basetenPolicy, metadataSource: 'https://huggingface.co/zai-org/GLM-5.2' }),
94
- model('glm47', 'GLM-4.7', 'Baseten', .6, 2.2, 'https://www.baseten.co/library/glm-4-7/', [...cohortScores('https://huggingface.co/zai-org/GLM-4.7', 'glm47-release', ['τ²-bench', 'MMLU-Pro', 'GPQA Diamond'], [87.4, 84.3, 85.7], 'GLM-4.7 release configurations; preserved thinking, adjusted user prompts and fixed Airline domain for τ²-bench.'), ...glmCodeScores([87.4, 84.9, 85.7])], { creator: 'Z.ai', origin: 'CN', openWeights: true, ...basetenPolicy, configuration: 'Thinking enabled', metadataSource: 'https://huggingface.co/zai-org/GLM-4.7' }),
95
- model('qwen36', 'Qwen 3.6 · 27B', 'Groq', .6, 3, groq, [...qwenScores(72.4, 60.6, 75.8, 87.8), ...qwen38Scores([50.4, 69.1, 87.8])], { creator: 'Alibaba', origin: 'CN', openWeights: true, availability: 'Preview', metadataSource: qwenCard }),
96
- model('qwen38', 'Qwen 3.8 · 27B', 'Groq', .8, 4, groq, qwen38Scores([56.9, 79.5, 89.2]), { creator: 'Alibaba', origin: 'CN', openWeights: true, availability: 'Preview', metadataSource: 'https://huggingface.co/Qwen/Qwen3.8-27B' }),
97
- model('jev', 'Jev 1.13', 'TypeSafe', .042, 0, 'https://docs.typesafe.ai/models', [], { outputType: 'decision' }),
98
- ];
99
- export const CONFIGURED_SERVICES = ['OpenAI', 'Anthropic', 'Google', 'Groq', 'Baseten', 'Mistral'];
100
- const byId = id => CATALOG.find(item => item.id === id);
101
- // Every workload is fictional application work. Names are plausible declarations,
102
- // never represented as discoveries from a visitor's repository.
103
- const APPLICATION_CODE_PROFILES = {
104
- 'actuarial.queries': { input: 14000, output: 700 },
105
- 'portfolio.reporting': { input: 18000, output: 900 },
106
- 'claims.reconciliation': { input: 5000, output: 2000 },
107
- 'reserves.analysis': { input: 6000, output: 2400 },
108
- };
109
- export const WORKLOADS = [
110
- ['claims.intake', 'Claim intake', 'gpt4o', ['parse_notice', 'extract_loss_date', 'normalize_address', 'link_policy', 'check_attachments', 'summarize_notice', 'prepare_case']],
111
- ['policy.extraction', 'Policy document extraction', 'gpt54', ['read_schedule', 'extract_limits', 'extract_deductibles', 'identify_endorsements', 'normalize_terms', 'link_sections', 'check_completeness']],
112
- ['coverage.review', 'Coverage review support', 'opus45', ['retrieve_wording', 'compare_endorsements', 'locate_exclusions', 'cite_conditions', 'summarize_scope', 'flag_ambiguity', 'prepare_review']],
113
- ['claims.correspondence', 'Claim correspondence', 'sonnet46', ['draft_acknowledgment', 'request_documents', 'summarize_progress', 'draft_follow_up', 'check_tone', 'cite_case_notes', 'prepare_handoff']],
114
- ['documents.verification', 'Document verification', 'gemini25pro', ['read_estimate', 'extract_line_items', 'match_invoices', 'check_totals', 'compare_versions', 'flag_missing_pages', 'prepare_evidence']],
115
- ['renewals.summary', 'Renewal preparation', 'gemini25flash', ['summarize_history', 'compare_schedules', 'list_changes', 'check_dates', 'extract_terms', 'prepare_brief', 'draft_reminder']],
116
- ['claims.tagging', 'Claim tagging', 'oss120', ['tag_document', 'label_channel', 'extract_reference', 'normalize_category', 'link_attachment', 'label_language', 'index_summary']],
117
- ['fraud.review', 'Fraud review support', 'glm53', ['summarize_signals', 'link_evidence', 'compare_statements', 'flag_inconsistency', 'build_timeline', 'prepare_case_note', 'route_to_reviewer']],
118
- ['broker.translation', 'Broker correspondence', 'mistral35', ['detect_language', 'translate_email', 'preserve_policy_terms', 'check_names', 'summarize_request', 'draft_reply', 'prepare_review']],
119
- ['claims.routing', 'Decision routing', 'gpt4o', ['route_notice', 'rank_queue', 'classify_document', 'score_completeness', 'flag_duplicates', 'choose_workflow', 'route_exception']],
120
- ['actuarial.queries', 'Actuarial query generation', 'sonnet45', ['read_schema', 'select_tables', 'generate_query', 'check_join_keys', 'validate_query', 'explain_result', 'prepare_review']],
121
- ['claims.reconciliation', 'Claim reconciliation scripts', 'sonnet45', ['read_line_items', 'normalize_columns', 'generate_transform', 'compare_totals', 'build_checks', 'explain_variance', 'prepare_review']],
122
- ['portfolio.reporting', 'Portfolio reporting queries', 'sonnet45', ['read_report_spec', 'map_dimensions', 'generate_query', 'check_aggregates', 'validate_periods', 'explain_result', 'prepare_report']],
123
- ['reserves.analysis', 'Reserve analysis scripts', 'sonnet45', ['read_triangle', 'normalize_periods', 'generate_calculation', 'check_assumptions', 'build_sensitivity', 'explain_projection', 'prepare_review']],
124
- ].map(([id, purpose, before, tasks]) => ({
125
- id, purpose, before: byId(before), configuredServices: CONFIGURED_SERVICES, tasks,
126
- benchmarkProfile: APPLICATION_CODE_PROFILES[id] ? 'Application code · tools, code generation, reasoning' : 'General application · tools, capabilities, reasoning',
127
- tokenProfile: APPLICATION_CODE_PROFILES[id] ?? null,
128
- requiredAxes: ['tools', 'capabilities', 'reasoning', ...(id === 'claims.routing' ? ['decisions'] : [])],
129
- }));
130
-
131
- export function modeledCost(model, usage) {
132
- return model && [model.input, model.output, usage.inputTokens, usage.outputTokens].every(value => Number.isFinite(value) && value >= 0)
133
- ? (model.input * usage.inputTokens + model.output * usage.outputTokens) / 1e6 : null;
134
- }
135
- export function assessReplacement(workload, candidate, usage) {
136
- const before = workload.before;
137
- const checks = workload.requiredAxes.flatMap(axis => axis === 'decisions' ? ['decisionAccuracy', 'calibration'] : [axis]).flatMap(axis => {
138
- const baselines = before.benchmarks.filter(item => item.axis === axis);
139
- const pairs = baselines.flatMap(a => candidate.benchmarks.filter(b => b.axis === axis && a.suite === b.suite && a.name === b.name && a.unit === b.unit && a.direction === b.direction).map(b => ({ a, b })));
140
- if (!pairs.length) return [{ axis, before: baselines[0] ?? null, after: candidate.benchmarks.find(item => item.axis === axis) ?? null, status: 'missing' }];
141
- // Check every matched metric, not the first passing score in a cohort.
142
- return pairs.map(({ a, b }) => {
143
- const comparable = Boolean(a.source && b.source && Number.isFinite(a.value) && Number.isFinite(b.value));
144
- const pass = comparable && (a.direction === 'lower' ? b.value <= a.value : b.value >= a.value);
145
- return { axis, before: a, after: b, status: !comparable ? 'missing' : pass ? 'pass' : 'regression' };
146
- });
147
- });
148
- const beforeCost = modeledCost(before, usage), afterCost = modeledCost(candidate, usage);
149
- const cheaper = beforeCost !== null && afterCost !== null && afterCost < beforeCost;
150
- // Only paired latency measurements with the same workload/configuration qualify.
151
- // No latency data is fabricated or inferred from token generation speed in this demo.
152
- const a = before.latency, b = candidate.latency;
153
- const faster = Boolean(a && b && a.suite && a.metric && a.suite === b.suite && a.metric === b.metric && a.source && b.source && Number.isFinite(a.value) && Number.isFinite(b.value) && b.value > 0 && b.value < a.value);
154
- const compatible = (workload.configuredServices ?? [before.provider]).includes(candidate.provider) && candidate.outputType === before.outputType && afterCost !== null;
155
- const issues = [];
156
- if (!(workload.configuredServices ?? [before.provider]).includes(candidate.provider)) issues.push('Service is not configured in this environment');
157
- if (candidate.outputType !== before.outputType) issues.push('Different output contract; application adaptation required');
158
- if (checks.some(check => check.status === 'missing')) issues.push('Comparable benchmark evidence is incomplete');
159
- if (checks.some(check => check.status === 'regression')) issues.push('A required benchmark regresses');
160
- if (afterCost === null) issues.push('Token price is unavailable');
161
- if (!cheaper && !faster) issues.push('No verified cost or latency improvement');
162
- return { candidate, checks, cheaper, faster, beforeCost, afterCost, compatible, issues, eligible: issues.length === 0 };
163
- }
164
- export function matchesPreference(model, preference) {
165
- return preference === 'zdr' ? model.confirmedZdr === true : preference === 'open' ? model.openWeights === true : preference === 'closed' ? model.openWeights === false : preference === 'usa' ? model.origin === 'US' : preference === 'china' ? model.origin === 'CN' : true;
166
- }
167
- export function selectReplacement(workload, usage, preference = 'default') {
168
- preference = preferenceFor(preference).value;
169
- const assessments = (workload.candidates ?? CATALOG).filter(candidate => candidate.id !== workload.before.id).map(candidate => assessReplacement(workload, candidate, usage));
170
- const allowed = item => item.eligible && (preference !== 'closed' || item.candidate.openWeights === false);
171
- const byCost = (a, b) => (a.afterCost ?? Infinity) - (b.afterCost ?? Infinity) || a.candidate.id.localeCompare(b.candidate.id);
172
- const qualified = assessments.filter(allowed).sort(byCost);
173
- const defaultChoice = assessments.filter(item => item.eligible).sort(byCost)[0]?.candidate ?? workload.before;
174
- // A preference first seeks a fully qualified match. Retaining an already
175
- // matching baseline needs no exception. Only then relax benchmark/efficiency
176
- // criteria, never the configured service, output contract or known pricing.
177
- let selected = preference === 'default' ? qualified[0] : qualified.find(item => matchesPreference(item.candidate, preference));
178
- if (!selected && preference !== 'default' && !matchesPreference(workload.before, preference)) {
179
- const axes = (item, status) => new Set(item.checks.filter(check => check.status === status).map(check => check.axis)).size;
180
- selected = assessments.filter(item => item.compatible && matchesPreference(item.candidate, preference)).sort((a, b) =>
181
- axes(a, 'regression') - axes(b, 'regression') || axes(a, 'missing') - axes(b, 'missing') ||
182
- Number(!a.cheaper && !a.faster) - Number(!b.cheaper && !b.faster) || byCost(a, b))[0];
183
- if (!selected && preference !== 'closed') selected = qualified[0];
184
- }
185
- const proposed = selected?.candidate ?? workload.before;
186
- const unresolved = preference === 'closed' && proposed.openWeights !== false;
187
- const after = unresolved ? null : proposed;
188
- const override = Boolean(after && selected && !selected.eligible);
189
- const overrideReasons = override ? selected.issues : [];
190
- const serviceChanged = Boolean(after && after.provider !== workload.before.provider);
191
- const modelChanged = Boolean(after && after.modelId !== workload.before.modelId);
192
- return {
193
- id: workload.id, purpose: workload.purpose, benchmarkProfile: workload.benchmarkProfile, tokenProfile: workload.tokenProfile, before: workload.before, after,
194
- changed: Boolean(after && selected), serviceChanged, modelChanged,
195
- checks: selected?.checks ?? [], assessments, qualifiedCount: qualified.length,
196
- preferenceMatched: Boolean(after && matchesPreference(after, preference)),
197
- preferenceChanged: Boolean(after && after.id !== defaultChoice.id),
198
- defaultChoice: defaultChoice.id, unresolved, override, overrideReasons,
199
- reason: unresolved ? 'No compatible closed-weight assignment in the reviewed catalog. Service, output contract and known pricing remain required.'
200
- : override ? `Selected for ${preferenceFor(preference).label}. No preferred replacement passed every primary check.`
201
- : selected ? `${modelChanged ? 'Selected benchmark scores match or improve' : 'Same model checkpoint and published benchmark scores; service changes'}; ${selected.cheaper ? 'modeled token cost is lower' : 'paired latency is lower'}.`
202
- : preference !== 'default' && matchesPreference(workload.before, preference) ? 'Baseline retained: already matches this preference; no qualifying preferred replacement.'
203
- : 'Baseline retained. No reviewed candidate passes every required check.',
204
- };
205
- }
@@ -1,151 +0,0 @@
1
- import { sampleSnapshot } from './sample.mjs';
2
- import { WORKLOADS, CATALOG, CONFIGURED_SERVICES, PREFERENCES, preferenceFor, selectReplacement } from './demo-policy.mjs';
3
-
4
- // Published standard, uncached text-token rates in USD per million tokens.
5
- // This catalog belongs to the fictional hosted demonstration, never live billing.
6
- export const PRICE_DATE = '2026-10-10';
7
- const openai = model => `https://developers.openai.com/api/docs/models/${model}`;
8
- const anthropic = 'https://platform.claude.com/docs/en/about-claude/pricing';
9
- const google = 'https://ai.google.dev/gemini-api/docs/pricing';
10
- const groq = 'https://console.groq.com/docs/models';
11
- const baseten = 'https://www.baseten.co/pricing/';
12
- const mistral = 'https://docs.mistral.ai/inference/pricing';
13
- const rate = (name, provider, input, output, source, creator = provider) => ({ name, provider, input, output, source, creator });
14
- export const PRICES = [
15
- rate('GPT-5.4', 'OpenAI', 2.5, 15, openai('gpt-5.4')),
16
- rate('GPT-5.4 mini', 'OpenAI', .75, 4.5, openai('gpt-5.4-mini')),
17
- rate('GPT-5.4 nano', 'OpenAI', .2, 1.25, openai('gpt-5.4-nano')),
18
- rate('Claude Opus 4.6', 'Anthropic', 5, 25, anthropic),
19
- rate('Claude Sonnet 4.6', 'Anthropic', 3, 15, anthropic),
20
- rate('Claude Haiku 4.5', 'Anthropic', 1, 5, anthropic),
21
- rate('Gemini 2.5 Pro', 'Google', 1.25, 10, google),
22
- rate('Gemini 2.5 Flash', 'Google', .3, 2.5, google),
23
- rate('Gemini 2.5 Flash-Lite', 'Google', .1, .4, google),
24
- rate('gpt-oss-120b', 'Groq', .15, .6, groq, 'OpenAI'),
25
- rate('gpt-oss-20b', 'Groq', .075, .3, groq, 'OpenAI'),
26
- rate('GLM-5.3', 'Baseten', 1.4, 4.4, baseten, 'Z.ai'),
27
- rate('GLM-5.3-Flash', 'Baseten', .15, .5, baseten, 'Z.ai'),
28
- rate('Mistral Medium 3.5', 'Mistral', 1.5, 7.5, mistral),
29
- rate('Mistral Small 4', 'Mistral', .15, .6, mistral),
30
- ];
31
- export function tokenCost(price, input, output) {
32
- if (!price || !Number.isFinite(input) || !Number.isFinite(output)) return null;
33
- return (input * price.input + output * price.output) / 1_000_000;
34
- }
35
- const priceFor = (provider, model) => PRICES.find(price => price.provider === provider && price.name === model);
36
- const REQUESTS_PER_GROUP = 1000;
37
-
38
- // A bounded set of explicit synthetic groups, not millions of duplicated rows.
39
- // Tokens and cost are group totals. The request count is sample-only metadata.
40
- export function enterpriseSnapshot(now = Date.now()) {
41
- const data = sampleSnapshot(now);
42
- data.simulation = { kind: 'enterprise-groups', days: 30, requestsPerGroup: REQUESTS_PER_GROUP, priceDate: PRICE_DATE, prices: PRICES };
43
- data.project = { name: 'enterprise-sample', scope: 'Fictional enterprise environment' };
44
- const replacements = new Map([
45
- ['GPT-5', priceFor('OpenAI', 'GPT-5.4')], ['GPT-5 mini', priceFor('OpenAI', 'GPT-5.4 mini')], ['GPT-5 nano', priceFor('OpenAI', 'GPT-5.4 nano')],
46
- ['Claude Sonnet 4.5', priceFor('Anthropic', 'Claude Sonnet 4.6')], ['Claude Opus 4.1', priceFor('Anthropic', 'Claude Opus 4.6')], ['Claude Haiku 3.5', priceFor('Anthropic', 'Claude Haiku 4.5')],
47
- ['DeepSeek R1', priceFor('Baseten', 'GLM-5.3')], ['Mistral Medium 3', priceFor('Mistral', 'Mistral Medium 3.5')],
48
- ]);
49
- const rename = (item, field) => { const price = replacements.get(item[field]); if (price) { item[field] = price.name; item.modelCreator = price.creator; } };
50
- data.models.forEach(item => rename(item, 'name'));
51
- data.deployments.forEach(item => { rename(item, 'model'); if (item.provider === 'Baseten') item.servingMode = 'shared'; });
52
- data.records.forEach((record, index) => {
53
- rename(record, 'model');
54
- record.sampleRequests = REQUESTS_PER_GROUP;
55
- record.occurredAt = new Date(now - (now - Date.parse(record.occurredAt)) * 30).toISOString();
56
- const shard = index % 5 + 1;
57
- record.agentId += `.worker-${shard}`;
58
- if (record.agent) record.agent += `.worker-${shard}`;
59
- record.taskId += `-partition-${shard}`;
60
- record.inputTokens *= REQUESTS_PER_GROUP;
61
- if (record.status === 'running') { record.status = 'completed'; record.outputTokens = 800; }
62
- if (record.outputTokens != null) record.outputTokens *= REQUESTS_PER_GROUP;
63
- record.cacheReadTokens = 0;
64
- record.cost = tokenCost(priceFor(record.provider, record.model), record.inputTokens, record.outputTokens);
65
- record.costKind = 'estimated';
66
- record.requestId = null;
67
- });
68
- for (const source of data.sources) {
69
- if (source.id === 'sample-activity') {
70
- source.records = 9_600_000;
71
- source.detail = 'Fictional request groups · 30 days';
72
- source.note = '9.6 million fictional requests represented by 9,600 groups of 1,000. Tokens and spend are group totals. Published uncached text-token prices are applied where available; other costs are unknown. No application or provider was contacted.';
73
- }
74
- }
75
- return data;
76
- }
77
-
78
- export function demoSnapshot(now = Date.now(), stage = 'before', preference = 'default') {
79
- if (!['before', 'after'].includes(stage)) throw new Error('Unknown comparison stage');
80
- preference = preferenceFor(preference).value;
81
- const data = enterpriseSnapshot(now);
82
- data.project = { name: 'insurance-tech-demo', scope: 'Fictional insurance operations application' };
83
- const usage = WORKLOADS.map(() => ({ inputTokens: 0, outputTokens: 0 }));
84
- data.records.forEach((record, index) => {
85
- record.assignmentId = index % WORKLOADS.length;
86
- const profile = WORKLOADS[record.assignmentId].tokenProfile;
87
- if (profile) {
88
- // Fixed fictional application profiles, independent of stage/preference.
89
- // Schema-heavy SQL prompts and longer reconciliation scripts have distinct
90
- // input/output mixes. Both include any billed reasoning output.
91
- const variation = .85 + (index * 17 % 31) / 100;
92
- record.inputTokens = Math.round(profile.input * variation) * REQUESTS_PER_GROUP;
93
- record.outputTokens = Math.round(profile.output * variation) * REQUESTS_PER_GROUP;
94
- }
95
- if (!record.outputTokens) record.outputTokens = (80 + index * 37 % 1600) * REQUESTS_PER_GROUP;
96
- usage[record.assignmentId].inputTokens += record.inputTokens;
97
- usage[record.assignmentId].outputTokens += record.outputTokens;
98
- });
99
- const plans = Object.fromEntries(PREFERENCES.map(({ value }) => [value, WORKLOADS.map((workload, index) => selectReplacement(workload, usage[index], value))]));
100
- const assignments = plans[preference];
101
- const signature = pairs => JSON.stringify(pairs.map(pair => pair.after?.id ?? null));
102
- const equivalentPreferences = Object.keys(plans).filter(value => value !== preference && signature(plans[value]) === signature(assignments));
103
- const feasible = assignments.every(pair => !pair.unresolved);
104
- data.simulation = { ...data.simulation, kind: 'comparison-groups', stage, preference, feasible, equivalentPreferences, catalogSize: CATALOG.filter(model => CONFIGURED_SERVICES.includes(model.provider)).length, configuredServices: CONFIGURED_SERVICES, assignments, prices: [...new Map(assignments.flatMap(pair => [pair.before, pair.after]).filter(Boolean).map(model => [model.id, model])).values()] };
105
- const models = new Map(), deployments = new Map();
106
- data.records.forEach((record, index) => {
107
- const assignmentId = record.assignmentId, assignment = assignments[assignmentId], workload = WORKLOADS[assignmentId];
108
- const price = stage === 'after' ? assignment.after ?? assignment.before : assignment.before, sequence = Math.floor(index / WORKLOADS.length);
109
- const worker = String(sequence % 20 + 1).padStart(2, '0');
110
- record.workload = `services/${workload.id.replaceAll('.', '/')}`;
111
- record.agentId = record.agent = `${workload.id}.worker-${worker}`;
112
- record.task = workload.tasks[sequence % workload.tasks.length];
113
- record.taskId = `${workload.id}.${record.task}.batch-${Math.floor(sequence / workload.tasks.length) % 10 + 1}`;
114
- record.model = price.name; record.modelCreator = price.creator; record.provider = price.provider;
115
- record.location = 'cloud'; record.status = 'completed'; record.durationMs = null;
116
- record.cost = tokenCost(price, record.inputTokens, record.outputTokens);
117
- record.beforeCost = tokenCost(assignment.before, record.inputTokens, record.outputTokens);
118
- record.afterCost = tokenCost(assignment.after, record.inputTokens, record.outputTokens);
119
- record.routeKind = ['direct', 'router', 'gateway'][index % 3];
120
- record.route = record.routeKind === 'direct' ? `${price.provider} API` : record.routeKind === 'router' ? 'insurance.router' : 'insurance-gateway';
121
- record.path = [{ name: record.route, kind: record.routeKind }];
122
- if (record.routeKind !== 'direct') record.path.push({ name: `${price.provider} API`, kind: 'direct' });
123
- record.deploymentId = `demo-deployment-${assignmentId}`;
124
- if (stage === 'after' && !assignment.after) return;
125
- models.set(price.id, { id: `demo-model-${price.id}`, name: price.name, provider: price.provider, modelCreator: price.creator, sourceId: 'sample-activity', location: 'cloud', state: 'observed' });
126
- deployments.set(assignmentId, { id: record.deploymentId, name: workload.id, model: price.name, modelCreator: price.creator, provider: price.provider, location: 'cloud', servingMode: 'shared', state: 'configured', sourceId: 'inventory', poolIds: [`pool-${price.provider.toLowerCase()}`] });
127
- });
128
- data.models = [...models.values()]; data.deployments = [...deployments.values()];
129
- const providers = CONFIGURED_SERVICES;
130
- data.inventory = [...providers.map(name => ({ name, kind: 'provider', location: 'cloud', state: 'configured', sourceId: 'inventory' })), { name: 'insurance.router', kind: 'router', location: 'cloud', state: 'configured' }, { name: 'insurance-gateway', kind: 'gateway', location: 'cloud', state: 'configured' }];
131
- data.capacityPools = data.capacityPools.filter(pool => providers.some(name => pool.id === `pool-${name.toLowerCase()}`));
132
- data.hosts = []; data.runtimeStats = []; delete data.machine;
133
- data.sources = data.sources.filter(source => ['sample-activity', 'sample-capacity', 'inventory'].includes(source.id));
134
- data.sources.find(source => source.id === 'sample-capacity').note = 'Fictional provider quota readings, held identical in both stages. They do not establish a change in latency, throughput or available capacity.';
135
- data.sources.find(source => source.id === 'sample-activity').note += ' Insurance workflow and worker names are fictional declarations. Assignments distinguish qualified replacements, retained baselines and explicit preference overrides. Overrides do not pass all primary criteria. Application quality and latency have not been measured.';
136
- data.simulation.windows = Object.fromEntries([0, 1, 6, 24, 168, 720].map(hours => {
137
- const rows = data.records.filter(record => !hours || Date.parse(record.occurredAt) >= now - hours * 3600000);
138
- const totals = selected => ({ requests: selected.length * REQUESTS_PER_GROUP, assignedRequests: selected.filter(record => assignments[record.assignmentId].after).length * REQUESTS_PER_GROUP, qualifiedRequests: selected.filter(record => assignments[record.assignmentId].after && !assignments[record.assignmentId].override).length * REQUESTS_PER_GROUP, overrideRequests: selected.filter(record => assignments[record.assignmentId].override).length * REQUESTS_PER_GROUP, beforeCost: selected.reduce((n, r) => n + r.beforeCost, 0), afterCost: feasible ? selected.reduce((n, r) => n + r.afterCost, 0) : null });
139
- return [hours, { ...totals(rows), workloads: assignments.map((pair, index) => {
140
- const selected = rows.filter(record => record.assignmentId === index);
141
- return { ...totals(selected), afterCost: pair.after ? selected.reduce((n, r) => n + tokenCost(pair.after, r.inputTokens, r.outputTokens), 0) : null };
142
- }) }];
143
- }));
144
- if (stage === 'after' && !feasible) {
145
- data.records = data.records.filter(record => assignments[record.assignmentId].after);
146
- const source = data.sources.find(source => source.id === 'sample-activity');
147
- source.records = data.records.length * REQUESTS_PER_GROUP;
148
- source.note += ' Incomplete After plan: only workloads with an assignment are included; preference overrides are identified in comparison metadata. Unresolved workloads remain in comparison metadata, without an After assignment or cost.';
149
- }
150
- return data;
151
- }
@@ -1,191 +0,0 @@
1
- # Building a harness adapter
2
-
3
- **Evaluation coordinator boundary:** this guide remains applicable to custom harnesses. Pi-specific examples refer to the historical Pi 0.87 evaluation adapter. The unreleased Pi 1.5.0 `openmerit/pi` export is instead a thin Scout adapter and does not implement `HarnessAdapter`; see [native MCP and SDK embedding](pi-extension.md#sdk-embedding).
4
-
5
- OpenMerit adapters are transport bridges. They do not reproduce OpenMerit's lifecycle, validation, persistence, or Pareto rules, and OpenMerit does not reproduce the harness's intelligence.
6
-
7
- ## Required components
8
-
9
- An adapter provides a `HarnessAdapter` with:
10
-
11
- - a stable harness descriptor;
12
- - the OpenMerit protocol versions it supports;
13
- - declared capabilities;
14
- - supported execution modes;
15
- - a `dispatch` method that accepts a canonical `OpenMeritIntent` and returns an acceptance receipt.
16
-
17
- ```ts
18
- import {
19
- OpenMeritCoordinator,
20
- ProjectStore,
21
- type HarnessAdapter,
22
- } from "openmerit/core";
23
- import { PROTOCOL_VERSION, OPENMERIT_SCHEMA_DIALECT } from "openmerit/protocol";
24
-
25
- const adapter: HarnessAdapter = {
26
- descriptor: {
27
- id: "my-harness",
28
- name: "My coding harness",
29
- version: "1.0.0",
30
- protocolVersions: [PROTOCOL_VERSION],
31
- capabilities: [
32
- "user_confirmation",
33
- "evaluation_authoring",
34
- "observability_instrumentation",
35
- "production_observation",
36
- "candidate_discovery",
37
- "challenger_execution",
38
- "frontier_calculation",
39
- "model_mutation",
40
- "post_swap_verification",
41
- "rollback",
42
- ],
43
- executionModes: ["interactive"],
44
- structuredOutput: {
45
- schemaDialect: OPENMERIT_SCHEMA_DIALECT,
46
- presentation: "adapter_validation",
47
- enforcement: "adapter",
48
- },
49
- automation: {
50
- supportedSignals: ["product_task_completed", "metric_window_available", "scheduled_tick", "model_catalog_changed", "verification_window_completed"],
51
- persistentScheduling: false,
52
- backgroundExecution: false,
53
- wakeupProvisioning: "none",
54
- },
55
- },
56
- async dispatch(intent) {
57
- await myHarness.enqueue(intent);
58
- return { accepted: true, harnessJobId: intent.id };
59
- },
60
- };
61
-
62
- const coordinator = new OpenMeritCoordinator(
63
- new ProjectStore(process.cwd()),
64
- adapter,
65
- );
66
- ```
67
-
68
- `ProjectStore` is the included local-filesystem backend. A remote or hosted integration can implement the `OpenMeritStore` interface instead.
69
-
70
- The confirmed task profile identifies the application LLM call under evaluation. Every metric, assessment, candidate, frontier artifact, and automation signal must carry that target ID; the harness's own model usage is orchestration data and must not be reported as product evidence. Application-provider cost, token, and latency telemetry belongs on the application signal or metric record.
71
-
72
- Setup may return `preliminaryModelLeads`: model IDs with sourced price estimates and reputation summaries. The field is optional under protocol 0.1 for older adapters. Core stores the leads and includes both the confirmed task profile and these leads in later discovery intent constraints, so a headless harness run does not depend on prior chat context. Leads are research priors, never measured candidate assessments.
73
-
74
- ## External telemetry export
75
-
76
- OpenMerit always writes its canonical, redacted audit stream locally. A host integration may pass `eventExporters` to `ProjectStore` to mirror those same safe events to OpenTelemetry, Langfuse, LangSmith, or another observability backend:
77
-
78
- ```ts
79
- const store = new ProjectStore(projectRoot, {
80
- eventExporters: [{
81
- id: "my-otel-bridge",
82
- async export(event) {
83
- await telemetry.emit("openmerit.audit", event);
84
- },
85
- }],
86
- });
87
- ```
88
-
89
- Exporters are not the durable source of truth. They are best effort, and failures are recorded locally without blocking OpenMerit decisions.
90
-
91
- ## Dispatching work
92
-
93
- ```ts
94
- const issued = await coordinator.issue("establish_evals");
95
- ```
96
-
97
- The coordinator builds the canonical intent, checks the descriptor's protocol and capabilities, creates durable state, records audit events, and calls the adapter. The harness decides how to execute the job.
98
-
99
- If a required capability is absent, dispatch is rejected before harness work starts.
100
-
101
- ## Reporting progress
102
-
103
- A harness may send durable lifecycle progress through `recordLifecycle`:
104
-
105
- ```ts
106
- await coordinator.recordLifecycle({
107
- protocolVersion: PROTOCOL_VERSION,
108
- intentId: issued.intent.id,
109
- status: "running",
110
- occurredAt: new Date().toISOString(),
111
- message: "Generating representative evaluation cases",
112
- });
113
- ```
114
-
115
- Use `waiting_for_user` when an interactive confirmation is required but cannot be completed in the current execution mode.
116
-
117
- ## Completing work
118
-
119
- Each intent kind has a typed output in `IntentOutputMap`. Return an `IntentResult<K>` with durable evidence references, then submit it to the coordinator:
120
-
121
- ```ts
122
- await coordinator.acceptResult(result);
123
- ```
124
-
125
- The coordinator validates the active intent, protocol version, evidence references, and intent-specific output, including applicable frontier and candidate-identity checks, before updating lifecycle state. Adapters must not duplicate this logic. The harness remains responsible for performing only the authorized work; result validation does not sandbox its tools or authenticate external evidence.
126
-
127
- ### Present the canonical result schema
128
-
129
- Import `completionToolSchemaFor(intent.kind)` from `openmerit/protocol` and present it through the harness's strongest structured-output mechanism. Do not recreate result schemas inside an adapter. A harness that supports changing tool definitions should specialize the completion tool whenever an intent becomes active; other harnesses may expose intent-specific tools or perform adapter-side validation.
130
-
131
- Declare the behavior in `HarnessDescriptor.structuredOutput`:
132
-
133
- - `schemaDialect` identifies the canonical OpenMerit JSON Schema dialect;
134
- - `presentation` states whether the adapter uses a dynamic tool, a static union, or adapter validation;
135
- - `enforcement` states whether validation occurs at the provider and harness, harness only, or adapter only.
136
-
137
- Schema validation catches malformed structure. The coordinator still owns semantic checks such as authorized candidate identity, metric readiness for frontier comparisons, lifecycle consistency, and Pareto conformance.
138
-
139
- ## Reporting normal work and catalogue changes
140
-
141
- After a real product task completes:
142
-
143
- ```ts
144
- await coordinator.recordTaskObservation({
145
- modelId: "vendor/model",
146
- });
147
-
148
- // Core has already checked baseline
149
- // sufficiency when the confirmed cadence became due. Inspect durable state
150
- // and dispatch only a pending downstream intent, if any.
151
- ```
152
-
153
- `recordTaskObservation` resolves the confirmed application target from project state. New adapters should prefer `recordAutomationSignal` so the task signal can carry the explicit `targetId`, durable evidence, and application-provider telemetry.
154
-
155
- When the harness's available model catalogue changes, compute a stable fingerprint and call `recordModelCatalogFingerprint`. The returned `reassessmentDue` flag tells the adapter whether the lifecycle justifies candidate discovery.
156
-
157
- New adapters should use `recordAutomationSignal` for product tasks, metric windows, scheduled ticks, catalogue observations, and verification windows. Signal IDs must remain stable across delivery retries. If the returned decision issues an intent, dispatch it through the coordinator. `recordTaskObservation` and `recordModelCatalogFingerprint` remain compatibility helpers.
158
-
159
- Call `reconcileAutomation` after configuration changes or when restoring a project. It returns an `AutomationPlan`. Implement `scheduleWakeup` only when the harness can genuinely arrange the requested wakeup. Declare persistent scheduling, background execution, supported signal sources, and provisioning requirements accurately in `HarnessDescriptor.automation`.
160
-
161
- If persistent scheduling is unavailable, expose the plan's active-session or external-scheduler fallback. Do not report a seven-day schedule as configured merely because the adapter checks policy when an interactive session starts.
162
-
163
- ## Capability mapping
164
-
165
- | Intent | Required capability |
166
- | --- | --- |
167
- | `establish_evals` | confirmation, evaluation authoring, observability instrumentation |
168
- | `instrument_observability` | observability instrumentation |
169
- | `run_assessment` | production observation |
170
- | `discover_candidates` | candidate discovery |
171
- | `run_challenger_trials` | challenger execution |
172
- | `calculate_frontier` | frontier calculation |
173
- | `investigate_regression` | production observation |
174
- | `apply_model_swap` | model mutation |
175
- | `verify_model_swap` | post-swap verification |
176
- | `rollback_model_swap` | rollback |
177
-
178
- ## Conformance checklist
179
-
180
- An adapter is ready when it:
181
-
182
- 1. advertises only capabilities it can actually execute;
183
- 2. preserves intent IDs and protocol versions;
184
- 3. never reports success without durable evidence;
185
- 4. returns the typed output for every supported intent;
186
- 5. performs model mutation only when the intent explicitly authorizes it;
187
- 6. reports ordinary product tasks without counting OpenMerit orchestration turns;
188
- 7. passes a durable issue-and-complete cycle using `OpenMeritCoordinator`;
189
- 8. proves the requested side effect, not merely a generated description.
190
-
191
- The fake non-Pi adapter in `packages/core/test/adapter-conformance.test.ts` is the executable reference.
@@ -1,61 +0,0 @@
1
- # Architecture
2
-
3
- **Source status:** the Pi 1.5.0 native Scout replacement is unreleased and unpublished. The protocol/core evaluation coordinator described here remains available for custom harnesses; it is not invoked by the new Pi adapter. The standalone evaluation CLI and Inference Terminal are unchanged.
4
-
5
- ## Design principle
6
-
7
- OpenMerit is an orchestrator, not an intelligence layer. The product application LLM call is the system under test; the coding harness is the evaluator and execution environment.
8
-
9
- ```text
10
- User or lifecycle event
11
- |
12
- v
13
- OpenMerit deterministic orchestration
14
- |
15
- | typed intent: outcome, constraints, evidence
16
- v
17
- Custom evaluation harness
18
- |
19
- | intelligent work, tools, evals, calculation
20
- v
21
- Typed result and durable evidence references
22
- |
23
- v
24
- OpenMerit validation, policy, persistence, next nudge
25
- ```
26
-
27
- An automatic OpenMerit action means automatic issuance of an approved intent. OpenMerit does not perform candidate research, evaluate quality, calculate a frontier, or mutate model configuration.
28
-
29
- ## Packages
30
-
31
- ### `openmerit/protocol`
32
-
33
- Defines the JSON-safe boundary: durable intents, evidence references, metrics, task profiles, candidate assessments, frontier snapshots, budgets, policies, state, and audit events.
34
-
35
- It also owns the versioned runtime schema registry for all intent results. Harness adapters consume these schemas rather than defining their own result shapes, report their structured-output capabilities, and may present the active schema using harness-native tools or adapter validation. A confirmed application LLM target is carried through profiles, metrics, candidates, frontier results, automation signals, and swaps; the harness model is not a valid substitute target.
36
-
37
- ### `openmerit/core`
38
-
39
- Contains deterministic behavior: choosing the next lifecycle intent according to confirmation and rollback policy, persisting state, and checking harness-produced frontiers against the normative definition. The verifier rejects invalid harness results; it is not a model-selection engine.
40
-
41
- `OpenMeritCoordinator` is the reusable adapter boundary. It constructs canonical intents, negotiates capabilities, records lifecycle progress and observations, validates results, and advances durable state. `OpenMeritStore` makes persistence replaceable; `ProjectStore` is the bundled filesystem backend.
42
-
43
- The coordinator also accepts idempotent application-target automation signals and evaluates the user-confirmed `AutomationCheckPolicy`. `buildAutomationPlan` tells an adapter which signals and wakeups are required and reports unsupported persistent scheduling honestly. Provisioning belongs to a custom adapter; the coordinator itself does not create operating-system schedulers or paid cloud workers. The removed Pi 0.87 adapter's LaunchAgent is historical functionality, not part of the new Scout connection.
44
-
45
- ### `openmerit/pi`
46
-
47
- Exports `createScoutPiExtension` and a default native Scout extension for Pi 1.5.0. It registers the authenticated Scout HTTP MCP server through Pi and adds shared rule/skill prompt sections. Pi owns discovery, tools, permissions, model and sessions; SDK hosts must load Pi's `createMcpExtension()` explicitly alongside it.
48
-
49
- The package no longer implements `HarnessAdapter`, exposes lifecycle commands, or schedules evaluation work. It shares credential and instruction code with the installer instead of duplicating an MCP client or evaluation coordinator. See [Pi installation and migration](pi-extension.md).
50
-
51
- ### Inference Terminal
52
-
53
- `packages/terminal` supplies a standalone `openmerit dash` executable, passive collectors, a loopback read-only HTTP API, and bundled browser assets. Stable npm 0.1.6 includes the terminal. The source interface and hosted dashboard follow the device’s light/dark preference; that appearance change is not included in npm 0.1.6. It does not invoke the coordinator or harness. One collector cycle reads existing metadata and runtime inventory; browser clients receive the same cached snapshot. Imported activity, runtime counters, and sampled whole-machine resources retain their separate scopes. Missing measurements remain unknown, and fictional sample data is isolated from the real snapshot. See the [terminal guide](inference-terminal.md) for the supported sources and JSON contract.
54
-
55
- ## Intelligence resources
56
-
57
- Models, web research, tools, evaluators, and fast decision systems such as Jev System One are harness resources. They are configured and invoked by the host harness. This keeps the coordinator provider-neutral and lets custom adapters choose their own implementation.
58
-
59
- ## Trust boundary
60
-
61
- The harness is trusted to perform the work and return honest evidence. OpenMerit reduces unsupported claims through structured outputs, evidence references, compatible profile revisions, complete comparisons, and conservative uncertainty handling. It cannot prove that an arbitrary external evidence URI contains truthful data.