dsh-harbor-evolution 0.7.3 → 0.8.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.
- package/README.md +10 -3
- package/index.js +40 -0
- package/lib/client.js +96 -17
- package/lib/dashboard.js +167 -15
- package/lib/evolution.js +189 -0
- package/lib/model-runtime.js +11 -7
- package/lib/session-diagnostic.js +320 -0
- package/lib/session-materializer.js +194 -0
- package/lib/session-projection.js +161 -0
- package/lib/session-redaction.js +311 -0
- package/lib/session-selection.js +294 -0
- package/lib/setup.js +9 -3
- package/package.json +6 -1
- package/schemas/dsh-session-observation.schema.json +69 -0
- package/schemas/evaluation-result-v2.schema.json +45 -0
- package/schemas/historical-evaluation-context.schema.json +66 -0
- package/schemas/historical-evaluation-summary.schema.json +49 -0
- package/schemas/historical-generation-batch.schema.json +76 -0
- package/skills/evolve-agent-with-harbor/SKILL.md +47 -7
- package/skills/evolve-agent-with-harbor/evals/evals.json +9 -6
- package/skills/evolve-agent-with-harbor/references/evaluator-upgrade.md +3 -1
package/lib/dashboard.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'node:path'
|
|
|
5
5
|
import { resolveWithin } from './evolution.js'
|
|
6
6
|
|
|
7
7
|
const SUMMARY_NAME = 'evaluation-summary.json'
|
|
8
|
+
const HISTORICAL_COMPLETION_NAME = 'historical-evaluation-complete.json'
|
|
8
9
|
const DEFAULT_JOB_PAGE_SIZE = 20
|
|
9
10
|
const MAX_JOB_PAGE_SIZE = 100
|
|
10
11
|
const MAX_JSON_BYTES = 2 * 1024 * 1024
|
|
@@ -107,19 +108,65 @@ function isObject(value) {
|
|
|
107
108
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
const CANDIDATE_JOB_KIND = 'candidate-evaluation'
|
|
112
|
+
const HISTORICAL_JOB_KIND = 'historical-generation-evaluation'
|
|
113
|
+
|
|
114
|
+
function normalizedJobKind(summary, context) {
|
|
115
|
+
const declared = summary?.job_kind ?? context?.job_kind
|
|
116
|
+
if (typeof declared === 'string' && declared) return declared
|
|
117
|
+
if (context?.protocol === 'historical-generation-evaluation-context/v1') return HISTORICAL_JOB_KIND
|
|
118
|
+
return CANDIDATE_JOB_KIND
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function coverageView(summary) {
|
|
122
|
+
if (isObject(summary?.coverage)) return summary.coverage
|
|
123
|
+
const total = Number(summary?.n_trials ?? 0)
|
|
124
|
+
const scored = Number(summary?.scored_trial_count ?? summary?.n_valid_scores ?? 0)
|
|
125
|
+
const unscored = Number(
|
|
126
|
+
summary?.unscored_trial_count
|
|
127
|
+
?? summary?.status_counts?.['completed-unscored']
|
|
128
|
+
?? 0,
|
|
129
|
+
)
|
|
130
|
+
return {
|
|
131
|
+
scored_trials: scored,
|
|
132
|
+
unscored_trials: unscored,
|
|
133
|
+
total_trials: total,
|
|
134
|
+
trial_rate: total ? scored / total : undefined,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function evaluatorMetaEvaluation(summary, context) {
|
|
139
|
+
return summary?.evaluator_meta_evaluation
|
|
140
|
+
?? context?.downstream_analysis?.evaluator_meta_evaluation
|
|
141
|
+
?? (normalizedJobKind(summary, context) === HISTORICAL_JOB_KIND
|
|
142
|
+
? { status: 'not-run', validation_report_ref: null }
|
|
143
|
+
: undefined)
|
|
144
|
+
}
|
|
145
|
+
|
|
110
146
|
function capabilityMap(summary, context, lifecycle, registry, stack) {
|
|
147
|
+
const jobKind = normalizedJobKind(summary, context)
|
|
148
|
+
const historicalGeneration = jobKind === HISTORICAL_JOB_KIND
|
|
111
149
|
const contextV2 = context?.schema_version === 2
|
|
150
|
+
const historicalContext = context?.schema_version === 1
|
|
151
|
+
&& context?.protocol === 'historical-generation-evaluation-context/v1'
|
|
112
152
|
const scoreValidity = summary?.schema_version === 3
|
|
153
|
+
|| summary?.schema_version === 4
|
|
113
154
|
return {
|
|
155
|
+
jobKind,
|
|
114
156
|
contextV2,
|
|
157
|
+
contextSupported: contextV2 || historicalContext,
|
|
158
|
+
historicalGeneration,
|
|
159
|
+
candidateEvaluation: !historicalGeneration,
|
|
115
160
|
trialLifecycle: lifecycle?.schema_version === 1,
|
|
116
161
|
scoreValidity,
|
|
117
162
|
evidenceProvenance: scoreValidity,
|
|
118
|
-
artifactRegistry: registry?.schema_version
|
|
119
|
-
|
|
163
|
+
artifactRegistry: [1, 2].includes(registry?.schema_version),
|
|
164
|
+
source: historicalGeneration,
|
|
165
|
+
compare: contextV2 && !historicalGeneration,
|
|
120
166
|
evaluatorGovernance: stack?.schema_version === 1,
|
|
121
|
-
|
|
122
|
-
|
|
167
|
+
evaluatorMetaEvaluation: evaluatorMetaEvaluation(summary, context),
|
|
168
|
+
gate: contextV2 && !historicalGeneration && summary?.mode === 'promotion-eligible',
|
|
169
|
+
readOnlyLegacy: !contextV2 && !historicalContext,
|
|
123
170
|
}
|
|
124
171
|
}
|
|
125
172
|
|
|
@@ -131,7 +178,7 @@ function primaryMetric(summary, contract) {
|
|
|
131
178
|
}
|
|
132
179
|
|
|
133
180
|
function progressView(summary, lifecycle, updatedAt) {
|
|
134
|
-
const total = Number(lifecycle?.dataset_total ?? summary?.n_trials ?? 0)
|
|
181
|
+
const total = Number(lifecycle?.dataset_total ?? summary?.n_trials ?? summary?.coverage?.total_trials ?? 0)
|
|
135
182
|
const lifecycleTrials = selectedLifecycleTrials(lifecycle)
|
|
136
183
|
const completed = lifecycle
|
|
137
184
|
? lifecycleTrials.filter(item => item.terminal).length
|
|
@@ -151,19 +198,53 @@ function progressView(summary, lifecycle, updatedAt) {
|
|
|
151
198
|
}
|
|
152
199
|
}
|
|
153
200
|
|
|
154
|
-
|
|
201
|
+
const HISTORICAL_COVERAGE_KEYS = [
|
|
202
|
+
'scored_trials', 'unscored_trials', 'total_trials', 'trial_rate',
|
|
203
|
+
'criterion_scored', 'criterion_total', 'criterion_rate',
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
function historicalCompletionValid(summary, completion, jobName) {
|
|
207
|
+
return (
|
|
208
|
+
summary?.schema_version === 4
|
|
209
|
+
&& summary?.job === jobName
|
|
210
|
+
&& summary?.job_kind === HISTORICAL_JOB_KIND
|
|
211
|
+
&& summary?.mode === 'diagnostic'
|
|
212
|
+
&& summary?.execution_mode === 'observe-existing'
|
|
213
|
+
&& summary?.artifact_validation?.valid === true
|
|
214
|
+
&& summary?.candidate === undefined
|
|
215
|
+
&& completion?.schema_version === 1
|
|
216
|
+
&& completion?.job_kind === HISTORICAL_JOB_KIND
|
|
217
|
+
&& completion?.status === 'completed'
|
|
218
|
+
&& completion?.valid === true
|
|
219
|
+
&& completion?.job === jobName
|
|
220
|
+
&& completion?.summary_path === SUMMARY_NAME
|
|
221
|
+
&& completion?.artifact_registry_path === 'artifact-registry.json'
|
|
222
|
+
&& HISTORICAL_COVERAGE_KEYS.every(key => (
|
|
223
|
+
typeof summary?.coverage?.[key] === 'number'
|
|
224
|
+
&& summary.coverage[key] === completion?.coverage?.[key]
|
|
225
|
+
))
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function jobStatus(summary, lifecycle, progress, jobKind, completion, jobName) {
|
|
155
230
|
if (summary?.__readError) return 'failed'
|
|
156
231
|
if (!summary && !lifecycle) return 'pending'
|
|
157
232
|
if (progress.active) return 'running'
|
|
158
233
|
if (!summary && lifecycle) return 'running'
|
|
234
|
+
if (summary?.artifact_validation?.valid === false) return 'failed'
|
|
235
|
+
if (
|
|
236
|
+
jobKind === HISTORICAL_JOB_KIND
|
|
237
|
+
&& !historicalCompletionValid(summary, completion, jobName)
|
|
238
|
+
) return 'failed'
|
|
159
239
|
if (Number(summary.n_infrastructure_exceptions ?? summary.n_exceptions ?? 0) > 0 || Number(summary.n_evaluation_exceptions ?? 0) > 0) return 'partial'
|
|
160
|
-
|
|
240
|
+
const invalidScores = Number(summary.n_invalid_scores ?? 0)
|
|
241
|
+
if (invalidScores > 0) return 'attention'
|
|
161
242
|
return 'completed'
|
|
162
243
|
}
|
|
163
244
|
|
|
164
245
|
async function readJob(jobsDir, entry, details) {
|
|
165
246
|
const directory = path.join(jobsDir, entry.name)
|
|
166
|
-
const [summary, contextFile, promotion, contract, lifecycle, registry, stack] = await Promise.all([
|
|
247
|
+
const [summary, contextFile, promotion, contract, lifecycle, registry, stack, completion] = await Promise.all([
|
|
167
248
|
readJson(path.join(directory, SUMMARY_NAME)),
|
|
168
249
|
readJson(path.join(directory, 'evaluation-context.json')),
|
|
169
250
|
readJson(path.join(directory, 'promotion-report.json')),
|
|
@@ -171,25 +252,38 @@ async function readJob(jobsDir, entry, details) {
|
|
|
171
252
|
readJson(path.join(directory, 'trial-lifecycle.json')),
|
|
172
253
|
readJson(path.join(directory, 'artifact-registry.json')),
|
|
173
254
|
readJson(path.join(directory, 'evaluation-stack-manifest.json')),
|
|
255
|
+
readJson(path.join(directory, HISTORICAL_COMPLETION_NAME)),
|
|
174
256
|
])
|
|
175
257
|
const evaluationContext = summary?.evaluation_context ?? contextFile
|
|
176
258
|
if (!evaluationContext && !summary && !lifecycle) return undefined
|
|
177
259
|
const updatedAt = details.mtime.toISOString()
|
|
178
260
|
const progress = progressView(summary, lifecycle, updatedAt)
|
|
261
|
+
const jobKind = normalizedJobKind(summary, evaluationContext)
|
|
179
262
|
const capabilities = capabilityMap(summary, evaluationContext, lifecycle, registry, stack)
|
|
263
|
+
const evaluationTarget = summary?.evaluation_target ?? evaluationContext?.evaluation_target
|
|
264
|
+
const generationSource = summary?.generation_source ?? evaluationContext?.generation_source
|
|
265
|
+
const coverage = coverageView(summary)
|
|
180
266
|
return {
|
|
181
267
|
name: entry.name,
|
|
182
268
|
updatedAt,
|
|
183
|
-
status: jobStatus(summary, lifecycle, progress),
|
|
269
|
+
status: jobStatus(summary, lifecycle, progress, jobKind, completion, entry.name),
|
|
270
|
+
jobKind,
|
|
184
271
|
mode: summary?.mode ?? evaluationContext?.mode,
|
|
272
|
+
executionMode: summary?.execution_mode ?? evaluationContext?.execution_mode,
|
|
185
273
|
nTrials: progress.total,
|
|
186
274
|
nDiscoveredTrials: Number(summary?.n_discovered_trials ?? lifecycle?.attempt_count ?? 0),
|
|
187
275
|
nValidScores: summary?.n_valid_scores,
|
|
188
276
|
nInvalidScores: summary?.n_invalid_scores,
|
|
277
|
+
nUnscoredTrials: Number(coverage.unscored_trials ?? 0),
|
|
189
278
|
nExceptions: Number(summary?.n_exceptions ?? 0),
|
|
190
279
|
primaryMetric: primaryMetric(summary, contract),
|
|
191
280
|
metrics: summary?.metrics ?? {},
|
|
192
281
|
candidate: summary?.candidate ?? evaluationContext?.candidate,
|
|
282
|
+
evaluationTarget,
|
|
283
|
+
generationSource,
|
|
284
|
+
generatorPopulation: evaluationTarget?.generator_population,
|
|
285
|
+
coverage,
|
|
286
|
+
evaluatorMetaEvaluation: evaluatorMetaEvaluation(summary, evaluationContext),
|
|
193
287
|
dataset: evaluationContext?.dataset,
|
|
194
288
|
evaluationContext,
|
|
195
289
|
progress,
|
|
@@ -353,6 +447,7 @@ const DETAIL_ARTIFACTS = {
|
|
|
353
447
|
diagnosis: 'diagnosis-report.json',
|
|
354
448
|
optimization: 'optimization-report.json',
|
|
355
449
|
promotion: 'promotion-report.json',
|
|
450
|
+
completion: HISTORICAL_COMPLETION_NAME,
|
|
356
451
|
}
|
|
357
452
|
|
|
358
453
|
function schemaIssue(key, value) {
|
|
@@ -360,8 +455,8 @@ function schemaIssue(key, value) {
|
|
|
360
455
|
if (value?.__readError) return value.__readError
|
|
361
456
|
if (!isObject(value)) return 'artifact must be an object'
|
|
362
457
|
const versions = {
|
|
363
|
-
summary: [2, 3], candidate: [1], dataset: [1], datasetPreview: [1], stack: [1], stackSources: [1], context: [1, 2], contract: [1],
|
|
364
|
-
doctor: [1], population: [1, 2], lifecycle: [1], registry: [1], diagnosis: [1], optimization: [1, 2], promotion: [2],
|
|
458
|
+
summary: [2, 3, 4], candidate: [1], dataset: [1], datasetPreview: [1], stack: [1], stackSources: [1], context: [1, 2], contract: [1],
|
|
459
|
+
doctor: [1], population: [1, 2, 3], lifecycle: [1], registry: [1, 2], diagnosis: [1, 2], optimization: [1, 2, 3], promotion: [2], completion: [1],
|
|
365
460
|
}[key]
|
|
366
461
|
if (versions && !versions.includes(value.schema_version)) return `schema_version must be one of ${versions.join(', ')}`
|
|
367
462
|
const required = {
|
|
@@ -369,8 +464,12 @@ function schemaIssue(key, value) {
|
|
|
369
464
|
stack: ['stack_id', 'version', 'digest', 'components', 'judge'], stackSources: ['stack_digest', 'components'], context: ['digest'], contract: ['contract_id', 'version', 'primary_metric', 'metrics'],
|
|
370
465
|
doctor: ['promotion_ready', 'findings'], population: ['population_size', 'groups', 'metrics'], lifecycle: ['dataset_total', 'trials'],
|
|
371
466
|
registry: ['artifacts'], diagnosis: ['diagnoses'], optimization: ['hypotheses'], promotion: ['decision', 'reasons', 'policy_digest'],
|
|
467
|
+
completion: ['job_kind', 'status', 'valid', 'job', 'summary_path', 'artifact_registry_path', 'coverage'],
|
|
372
468
|
}[key] ?? []
|
|
373
|
-
const
|
|
469
|
+
const requiredFields = key === 'population' && value.schema_version === 3
|
|
470
|
+
? ['population_size', 'coverage', 'metrics']
|
|
471
|
+
: required
|
|
472
|
+
const missing = requiredFields.filter(field => value[field] === undefined)
|
|
374
473
|
return missing.length ? `missing fields: ${missing.join(', ')}` : undefined
|
|
375
474
|
}
|
|
376
475
|
|
|
@@ -390,10 +489,35 @@ export async function readJobDetail(config, args) {
|
|
|
390
489
|
return [key, value === undefined ? { status: 'unavailable', reason: 'capability-not-produced' } : issue ? { status: 'invalid', error: issue } : { status: 'valid' }]
|
|
391
490
|
}))
|
|
392
491
|
const context = artifacts.context ?? values[Object.keys(DETAIL_ARTIFACTS).indexOf('context')]
|
|
492
|
+
const summary = values[Object.keys(DETAIL_ARTIFACTS).indexOf('summary')]
|
|
493
|
+
const jobKind = normalizedJobKind(summary, context)
|
|
494
|
+
if (
|
|
495
|
+
jobKind === HISTORICAL_JOB_KIND
|
|
496
|
+
&& !historicalCompletionValid(summary, artifacts.completion, job)
|
|
497
|
+
) {
|
|
498
|
+
validation.completion = {
|
|
499
|
+
status: 'invalid',
|
|
500
|
+
error: 'Historical completion sentinel is missing, stale, or inconsistent with the Summary',
|
|
501
|
+
}
|
|
502
|
+
}
|
|
393
503
|
const capabilities = capabilityMap(
|
|
394
|
-
|
|
504
|
+
summary, context, artifacts.lifecycle, artifacts.registry, artifacts.stack,
|
|
395
505
|
)
|
|
396
|
-
|
|
506
|
+
const evaluationTarget = summary?.evaluation_target ?? context?.evaluation_target
|
|
507
|
+
return {
|
|
508
|
+
schemaVersion: 3,
|
|
509
|
+
job,
|
|
510
|
+
jobKind,
|
|
511
|
+
evaluationTarget,
|
|
512
|
+
generationSource: summary?.generation_source ?? context?.generation_source,
|
|
513
|
+
generatorPopulation: evaluationTarget?.generator_population,
|
|
514
|
+
executionMode: summary?.execution_mode ?? context?.execution_mode,
|
|
515
|
+
coverage: coverageView(summary),
|
|
516
|
+
evaluatorMetaEvaluation: evaluatorMetaEvaluation(summary, context),
|
|
517
|
+
capabilities,
|
|
518
|
+
artifacts,
|
|
519
|
+
validation,
|
|
520
|
+
}
|
|
397
521
|
}
|
|
398
522
|
|
|
399
523
|
function selectedLifecycleTrials(lifecycle) {
|
|
@@ -408,13 +532,15 @@ function selectedLifecycleTrials(lifecycle) {
|
|
|
408
532
|
function normalizeTrial(trial, order) {
|
|
409
533
|
const score = trial.score ?? { value: undefined, valid: trial.exception ? false : true, invalid_reasons: trial.exception ? ['infrastructure-error'] : [] }
|
|
410
534
|
const datasetOrder = Number(trial.datasetOrder ?? trial.dataset_order ?? order)
|
|
535
|
+
const status = trial.status ?? trial.phase ?? (trial.exception ? 'infrastructure-error' : 'completed')
|
|
411
536
|
return {
|
|
412
537
|
id: trial.id ?? trial.execution_id ?? `dataset-${datasetOrder}`,
|
|
413
538
|
name: trial.name ?? trial.trial_name ?? trial.dataset_trial ?? trial.trial,
|
|
414
539
|
datasetTrial: trial.datasetTrial ?? trial.dataset_trial ?? trial.trial,
|
|
415
540
|
datasetOrder,
|
|
416
541
|
attempt: Number(trial.attempt ?? 1),
|
|
417
|
-
status
|
|
542
|
+
status,
|
|
543
|
+
scoringStatus: status === 'completed-unscored' ? 'unscored' : score.valid ? 'scored' : 'invalid',
|
|
418
544
|
terminal: trial.terminal ?? true,
|
|
419
545
|
updatedAt: trial.updatedAt ?? trial.updated_at,
|
|
420
546
|
score,
|
|
@@ -737,6 +863,32 @@ export async function readComparison(config, args) {
|
|
|
737
863
|
if (!baseline || baseline.__readError || !candidate || candidate.__readError) throw new Error('Both Job summaries are required')
|
|
738
864
|
const baselineContext = baseline.evaluation_context ?? await readJson(path.join(jobDirectory(config, baselineJob), 'evaluation-context.json'))
|
|
739
865
|
const candidateContext = candidate.evaluation_context ?? await readJson(path.join(jobDirectory(config, candidateJob), 'evaluation-context.json'))
|
|
866
|
+
const baselineKind = normalizedJobKind(baseline, baselineContext)
|
|
867
|
+
const candidateKind = normalizedJobKind(candidate, candidateContext)
|
|
868
|
+
if (baselineKind !== CANDIDATE_JOB_KIND || candidateKind !== CANDIDATE_JOB_KIND) {
|
|
869
|
+
const error = {
|
|
870
|
+
code: 'UNSUPPORTED_JOB_KIND_FOR_PROMOTION',
|
|
871
|
+
message: 'Historical Generation Evaluation Jobs are diagnostic evidence and cannot be used as a Candidate baseline, comparison, or Promotion Gate input.',
|
|
872
|
+
}
|
|
873
|
+
return {
|
|
874
|
+
schemaVersion: 1,
|
|
875
|
+
baselineJob,
|
|
876
|
+
candidateJob,
|
|
877
|
+
baselineJobKind: baselineKind,
|
|
878
|
+
candidateJobKind: candidateKind,
|
|
879
|
+
comparable: false,
|
|
880
|
+
comparabilityReasons: [error],
|
|
881
|
+
metrics: {},
|
|
882
|
+
population: {},
|
|
883
|
+
improvedTrials: [],
|
|
884
|
+
regressedTrials: [],
|
|
885
|
+
newExceptions: [],
|
|
886
|
+
artifactRegressions: [],
|
|
887
|
+
gateEligibility: 'not-applicable',
|
|
888
|
+
error,
|
|
889
|
+
note: 'Convert reviewed badcases into a fixed regression Dataset before running Candidate comparison or Gate.',
|
|
890
|
+
}
|
|
891
|
+
}
|
|
740
892
|
const reasons = []
|
|
741
893
|
if (baselineContext?.schema_version !== 2 || candidateContext?.schema_version !== 2) reasons.push('Context v2 is required')
|
|
742
894
|
if (!baselineContext?.digest || baselineContext.digest !== candidateContext?.digest) reasons.push('Evaluation Context differs; establish a fresh baseline')
|
package/lib/evolution.js
CHANGED
|
@@ -66,6 +66,73 @@ export function makeJobName(manifest, now = new Date()) {
|
|
|
66
66
|
return `${identity}-${suffix}`
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export function makeHistoricalJobName(batch, now = new Date()) {
|
|
70
|
+
const timestamp = now.toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/[-:]/g, '')
|
|
71
|
+
const digest = String(batch?.digest ?? '').replace(/^sha256:/, '').slice(0, 8) || 'historical'
|
|
72
|
+
return `dsh-session-history-${timestamp}-${digest}`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const HISTORICAL_COVERAGE_KEYS = [
|
|
76
|
+
'scored_trials',
|
|
77
|
+
'unscored_trials',
|
|
78
|
+
'total_trials',
|
|
79
|
+
'trial_rate',
|
|
80
|
+
'criterion_scored',
|
|
81
|
+
'criterion_total',
|
|
82
|
+
'criterion_rate',
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
function historicalCoverageMatches(summaryCoverage, completionCoverage) {
|
|
86
|
+
if (!summaryCoverage || !completionCoverage) return false
|
|
87
|
+
return HISTORICAL_COVERAGE_KEYS.every(key => (
|
|
88
|
+
typeof summaryCoverage[key] === 'number'
|
|
89
|
+
&& summaryCoverage[key] === completionCoverage[key]
|
|
90
|
+
))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Harbor 0.21 treats Job-plugin finalization failures as warnings, so a zero
|
|
95
|
+
* process exit is not proof that Historical artifacts completed. Validate the
|
|
96
|
+
* fresh plugin-owned sentinel and its core Summary cross-links fail-closed.
|
|
97
|
+
*/
|
|
98
|
+
export function assertHistoricalCompletion(summary, completion, {
|
|
99
|
+
jobName,
|
|
100
|
+
batchDigest,
|
|
101
|
+
batchId,
|
|
102
|
+
recordCount,
|
|
103
|
+
}) {
|
|
104
|
+
const summaryValid = (
|
|
105
|
+
summary?.schema_version === 4
|
|
106
|
+
&& summary?.job === jobName
|
|
107
|
+
&& summary?.job_kind === 'historical-generation-evaluation'
|
|
108
|
+
&& summary?.mode === 'diagnostic'
|
|
109
|
+
&& summary?.execution_mode === 'observe-existing'
|
|
110
|
+
&& summary?.evaluation_target?.digest === batchDigest
|
|
111
|
+
&& summary?.evaluation_target?.batch_id === batchId
|
|
112
|
+
&& summary?.evaluation_target?.record_count === recordCount
|
|
113
|
+
&& summary?.n_trials === recordCount
|
|
114
|
+
&& summary?.coverage?.total_trials === recordCount
|
|
115
|
+
&& summary?.artifact_validation?.valid === true
|
|
116
|
+
&& summary?.candidate === undefined
|
|
117
|
+
)
|
|
118
|
+
if (!summaryValid) {
|
|
119
|
+
throw new Error('HISTORICAL_JOB_IDENTITY_INVALID: the Summary is not a validated, Candidate-free Historical Generation Evaluation for this Batch')
|
|
120
|
+
}
|
|
121
|
+
const completionValid = (
|
|
122
|
+
completion?.schema_version === 1
|
|
123
|
+
&& completion?.job_kind === 'historical-generation-evaluation'
|
|
124
|
+
&& completion?.status === 'completed'
|
|
125
|
+
&& completion?.valid === true
|
|
126
|
+
&& completion?.job === jobName
|
|
127
|
+
&& completion?.summary_path === 'evaluation-summary.json'
|
|
128
|
+
&& completion?.artifact_registry_path === 'artifact-registry.json'
|
|
129
|
+
&& historicalCoverageMatches(summary.coverage, completion.coverage)
|
|
130
|
+
)
|
|
131
|
+
if (!completionValid) {
|
|
132
|
+
throw new Error('HISTORICAL_JOB_ARTIFACT_VALIDATION_FAILED: the completion sentinel is missing, stale, or inconsistent with the validated Summary')
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
69
136
|
async function cliJson(config, args, { allowedExitCodes = [0], input } = {}) {
|
|
70
137
|
let result
|
|
71
138
|
try {
|
|
@@ -374,6 +441,128 @@ export async function runEvaluation(config, args, modelRuntime) {
|
|
|
374
441
|
}
|
|
375
442
|
}
|
|
376
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Materialize an immutable Historical Generation Batch before Harbor creates
|
|
446
|
+
* its Job, then run a deterministic Observation Adapter. This path never
|
|
447
|
+
* snapshots or executes a Candidate.
|
|
448
|
+
*/
|
|
449
|
+
export async function runHistoricalEvaluation(config, args, modelRuntime) {
|
|
450
|
+
const projectRoot = path.resolve(config.projectRoot)
|
|
451
|
+
const batchPath = resolveWithin(projectRoot, args.batchPath, 'batchPath')
|
|
452
|
+
const batchDir = resolveWithin(projectRoot, args.batchDir ?? path.dirname(batchPath), 'batchDir')
|
|
453
|
+
const output = resolveWithin(projectRoot, path.join(batchDir, 'dataset'), 'historicalDataset')
|
|
454
|
+
if (!args.judgeBinding?.provider || !args.judgeBinding?.model) {
|
|
455
|
+
throw new Error('Historical Judge model binding is required before materialization')
|
|
456
|
+
}
|
|
457
|
+
const materialized = await cliJson(config, [
|
|
458
|
+
'historical', 'materialize',
|
|
459
|
+
'--project-root', projectRoot,
|
|
460
|
+
'--batch', batchPath,
|
|
461
|
+
'--output', output,
|
|
462
|
+
'--judge-provider', args.judgeBinding.provider,
|
|
463
|
+
'--judge-model', args.judgeBinding.model,
|
|
464
|
+
...(args.judgeBinding.reasoning_effort === undefined
|
|
465
|
+
? []
|
|
466
|
+
: ['--judge-reasoning-effort', args.judgeBinding.reasoning_effort]),
|
|
467
|
+
])
|
|
468
|
+
const dataset = resolveWithin(projectRoot, materialized.dataset_path ?? output, 'historicalDataset')
|
|
469
|
+
const stack = resolveWithin(projectRoot, materialized.stack_path, 'historicalStack')
|
|
470
|
+
const batch = JSON.parse(await readFile(batchPath, 'utf8'))
|
|
471
|
+
const jobs = resolveWithin(projectRoot, config.jobsDir, 'jobsDir')
|
|
472
|
+
const jobName = args.jobName ?? makeHistoricalJobName(batch)
|
|
473
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(jobName)) {
|
|
474
|
+
throw new Error('jobName contains unsupported characters')
|
|
475
|
+
}
|
|
476
|
+
const agentImportPath = config.historicalAgentImportPath
|
|
477
|
+
?? 'harbor_dsh_evolution.session_agent:SessionObservationAgent'
|
|
478
|
+
const pluginImportPath = config.historicalPluginImportPath
|
|
479
|
+
?? 'dsh-historical-evaluation'
|
|
480
|
+
const harborArgs = [
|
|
481
|
+
'run', '-y', '-p', dataset,
|
|
482
|
+
'-a', agentImportPath,
|
|
483
|
+
'--job-name', jobName,
|
|
484
|
+
'--jobs-dir', jobs,
|
|
485
|
+
'--plugin', pluginImportPath,
|
|
486
|
+
'--plugin-kwarg', `batch_path=${batchPath}`,
|
|
487
|
+
'--plugin-kwarg', `dataset_path=${dataset}`,
|
|
488
|
+
'--plugin-kwarg', `stack_path=${stack}`,
|
|
489
|
+
'--plugin-kwarg', `project_root=${projectRoot}`,
|
|
490
|
+
'--plugin-kwarg', 'mode=diagnostic',
|
|
491
|
+
]
|
|
492
|
+
const lease = await modelRuntime.openLease(args.judgeBinding, {
|
|
493
|
+
candidateDigest: batch.digest,
|
|
494
|
+
jobName,
|
|
495
|
+
})
|
|
496
|
+
const jobDir = path.join(jobs, jobName)
|
|
497
|
+
try {
|
|
498
|
+
let processResult
|
|
499
|
+
try {
|
|
500
|
+
processResult = await runProcess(config.harborBin, harborArgs, {
|
|
501
|
+
cwd: projectRoot,
|
|
502
|
+
timeoutMs: config.timeoutMs,
|
|
503
|
+
env: {
|
|
504
|
+
...process.env,
|
|
505
|
+
...(config.pythonPath ? { PYTHONPATH: config.pythonPath } : {}),
|
|
506
|
+
HSE_JUDGE_GATEWAY_URL: lease.endpoint,
|
|
507
|
+
HSE_JUDGE_GATEWAY_TOKEN: lease.token,
|
|
508
|
+
HSE_JUDGE_GATEWAY_PROVIDER: lease.candidateProvider,
|
|
509
|
+
HSE_JUDGE_GATEWAY_INFO: JSON.stringify({
|
|
510
|
+
protocol: lease.protocol,
|
|
511
|
+
candidate_digest: batch.digest,
|
|
512
|
+
job: jobName,
|
|
513
|
+
binding: {
|
|
514
|
+
provider: args.judgeBinding.provider,
|
|
515
|
+
model: args.judgeBinding.model,
|
|
516
|
+
...(args.judgeBinding.reasoning_effort === undefined
|
|
517
|
+
? {}
|
|
518
|
+
: { reasoning_effort: args.judgeBinding.reasoning_effort }),
|
|
519
|
+
},
|
|
520
|
+
model_info: lease.modelInfo,
|
|
521
|
+
}),
|
|
522
|
+
HSE_JUDGE_GATEWAY_PROTOCOL: lease.protocol,
|
|
523
|
+
},
|
|
524
|
+
})
|
|
525
|
+
} catch (error) {
|
|
526
|
+
throw await explainHarborFailure(error, jobDir)
|
|
527
|
+
}
|
|
528
|
+
let summary
|
|
529
|
+
let completion
|
|
530
|
+
try {
|
|
531
|
+
summary = JSON.parse(await readFile(path.join(jobDir, 'evaluation-summary.json'), 'utf8'))
|
|
532
|
+
completion = JSON.parse(await readFile(path.join(jobDir, 'historical-evaluation-complete.json'), 'utf8'))
|
|
533
|
+
} catch (error) {
|
|
534
|
+
throw new Error(
|
|
535
|
+
`HISTORICAL_JOB_INCOMPLETE: Harbor exited successfully but the Historical plugin did not write its summary/completion sentinel (${error.message})`,
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
assertHistoricalCompletion(summary, completion, {
|
|
539
|
+
jobName,
|
|
540
|
+
batchDigest: batch.digest,
|
|
541
|
+
batchId: batch.batch_id,
|
|
542
|
+
recordCount: Array.isArray(batch.records) ? batch.records.length : undefined,
|
|
543
|
+
})
|
|
544
|
+
return {
|
|
545
|
+
judgeModelBinding: {
|
|
546
|
+
provider: args.judgeBinding.provider,
|
|
547
|
+
model: args.judgeBinding.model,
|
|
548
|
+
...(args.judgeBinding.reasoning_effort === undefined
|
|
549
|
+
? {}
|
|
550
|
+
: { reasoning_effort: args.judgeBinding.reasoning_effort }),
|
|
551
|
+
},
|
|
552
|
+
job: path.relative(projectRoot, jobDir).split(path.sep).join('/'),
|
|
553
|
+
summary,
|
|
554
|
+
completion,
|
|
555
|
+
materialized: {
|
|
556
|
+
dataset: path.relative(projectRoot, dataset).split(path.sep).join('/'),
|
|
557
|
+
stack: path.relative(projectRoot, stack).split(path.sep).join('/'),
|
|
558
|
+
},
|
|
559
|
+
process: { code: processResult.code },
|
|
560
|
+
}
|
|
561
|
+
} finally {
|
|
562
|
+
await lease.close()
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
377
566
|
export async function readEvaluation(config, args) {
|
|
378
567
|
const jobDir = resolveWithin(config.projectRoot, args.jobPath, 'jobPath')
|
|
379
568
|
return JSON.parse(await readFile(path.join(jobDir, 'evaluation-summary.json'), 'utf8'))
|
package/lib/model-runtime.js
CHANGED
|
@@ -74,13 +74,17 @@ export class CandidateModelRuntime {
|
|
|
74
74
|
this.config = config
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
async
|
|
77
|
+
async resolveCurrent() {
|
|
78
78
|
const inherited = this.ctx.agentDefaultModel.currentSelection()
|
|
79
|
-
|
|
79
|
+
return this.resolve({
|
|
80
80
|
candidateProvider: inherited.provider,
|
|
81
81
|
candidateModel: inherited.model,
|
|
82
82
|
candidateReasoningEffort: inherited.reasoningEffort,
|
|
83
|
-
})
|
|
83
|
+
}, undefined, { ignoreConfigured: true })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async currentBinding() {
|
|
87
|
+
const binding = await this.resolveCurrent()
|
|
84
88
|
return {
|
|
85
89
|
schema_version: 1,
|
|
86
90
|
source: 'skill-agent-default',
|
|
@@ -92,14 +96,14 @@ export class CandidateModelRuntime {
|
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
async resolve(args = {}, pinnedBinding) {
|
|
99
|
+
async resolve(args = {}, pinnedBinding, { ignoreConfigured = false } = {}) {
|
|
96
100
|
const explicitProvider = nonBlank(args.candidateProvider)
|
|
97
101
|
const explicitModel = nonBlank(args.candidateModel)
|
|
98
102
|
if (Boolean(explicitProvider) !== Boolean(explicitModel)) {
|
|
99
103
|
throw new Error('candidateProvider and candidateModel must be supplied together')
|
|
100
104
|
}
|
|
101
|
-
const configuredProvider = nonBlank(this.config.candidateProvider)
|
|
102
|
-
const configuredModel = nonBlank(this.config.candidateModel)
|
|
105
|
+
const configuredProvider = ignoreConfigured ? undefined : nonBlank(this.config.candidateProvider)
|
|
106
|
+
const configuredModel = ignoreConfigured ? undefined : nonBlank(this.config.candidateModel)
|
|
103
107
|
if (Boolean(configuredProvider) !== Boolean(configuredModel)) {
|
|
104
108
|
throw new Error('Harbor candidateProvider and candidateModel configuration must be supplied together')
|
|
105
109
|
}
|
|
@@ -129,7 +133,7 @@ export class CandidateModelRuntime {
|
|
|
129
133
|
const provider = pinnedProvider ?? explicitProvider ?? configuredProvider ?? inherited.provider
|
|
130
134
|
const model = pinnedModel ?? explicitModel ?? configuredModel ?? inherited.model
|
|
131
135
|
const explicitReasoning = nonBlank(args.candidateReasoningEffort)
|
|
132
|
-
const configuredReasoning = nonBlank(this.config.candidateReasoningEffort)
|
|
136
|
+
const configuredReasoning = ignoreConfigured ? undefined : nonBlank(this.config.candidateReasoningEffort)
|
|
133
137
|
const canInheritReasoning = provider === inherited.provider && model === inherited.model
|
|
134
138
|
const reasoningEffort = pinnedProvider
|
|
135
139
|
? pinnedReasoning
|