@artemiskit/core 0.5.0 → 0.5.2
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/CHANGELOG.md +16 -0
- package/dist/artifacts/manifest.d.ts +5 -1
- package/dist/artifacts/manifest.d.ts.map +1 -1
- package/dist/artifacts/types.d.ts +87 -0
- package/dist/artifacts/types.d.ts.map +1 -1
- package/dist/index.js +622 -400
- package/dist/provenance/execution-provenance.d.ts +11 -0
- package/dist/provenance/execution-provenance.d.ts.map +1 -0
- package/dist/provenance/index.d.ts +1 -0
- package/dist/provenance/index.d.ts.map +1 -1
- package/dist/runner/executor.d.ts.map +1 -1
- package/dist/runner/runner.d.ts.map +1 -1
- package/dist/runner/types.d.ts +15 -1
- package/dist/runner/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/artifacts/manifest.test.ts +123 -1
- package/src/artifacts/manifest.ts +25 -29
- package/src/artifacts/types.ts +262 -0
- package/src/provenance/execution-provenance.test.ts +59 -0
- package/src/provenance/execution-provenance.ts +53 -0
- package/src/provenance/index.ts +1 -0
- package/src/runner/executor.test.ts +60 -0
- package/src/runner/executor.ts +145 -11
- package/src/runner/release-validation.test.ts +36 -0
- package/src/runner/runner.ts +31 -1
- package/src/runner/types.ts +9 -1
- package/src/scenario/schema.ts +1 -1
package/src/artifacts/types.ts
CHANGED
|
@@ -65,6 +65,33 @@ export interface WorkloadIdentity {
|
|
|
65
65
|
rubric: ContentIdentity;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/** Bounded target identity captured from a case execution. */
|
|
69
|
+
export interface CaseTargetEvidence {
|
|
70
|
+
provider: string;
|
|
71
|
+
requested_model?: string;
|
|
72
|
+
/** Model identifiers returned by the target provider during this case. */
|
|
73
|
+
observed_models?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Requested and observed execution configuration for a complete run. */
|
|
77
|
+
export interface ExecutionProvenance {
|
|
78
|
+
schema_version: '1';
|
|
79
|
+
target: {
|
|
80
|
+
provider: string;
|
|
81
|
+
requested_models?: string[];
|
|
82
|
+
observed_models?: string[];
|
|
83
|
+
generation?: {
|
|
84
|
+
temperature?: number;
|
|
85
|
+
max_tokens?: number;
|
|
86
|
+
seed?: number;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
/** Judge/evaluator model identities, never combined with target identity. */
|
|
90
|
+
evaluator?: {
|
|
91
|
+
models?: string[];
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
68
95
|
// ============================================================================
|
|
69
96
|
// Case Result Types
|
|
70
97
|
// ============================================================================
|
|
@@ -97,6 +124,39 @@ export interface CaseEvaluationEvidence {
|
|
|
97
124
|
};
|
|
98
125
|
}
|
|
99
126
|
|
|
127
|
+
/** A bounded record of one execution in a retry chain. */
|
|
128
|
+
export interface CaseAttemptEvidence {
|
|
129
|
+
attempt_id: string;
|
|
130
|
+
retry_chain_id: string;
|
|
131
|
+
/** One-based coordinate within a deliberately independent repetition. */
|
|
132
|
+
repetition_index: number;
|
|
133
|
+
/** One-based coordinate within this retry chain. */
|
|
134
|
+
attempt_number: number;
|
|
135
|
+
status: CaseEvaluationStatus;
|
|
136
|
+
/** Only the terminal measurement can contribute to an outcome rate. */
|
|
137
|
+
included_in_outcome: boolean;
|
|
138
|
+
latency_ms: number;
|
|
139
|
+
/** Sanitized classification, never arbitrary provider error text. */
|
|
140
|
+
error_code?: 'timeout' | 'target_error' | 'tool_error';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Declared retry and repetition context for a run. */
|
|
144
|
+
export interface RunAttemptEvidence {
|
|
145
|
+
schema_version: '1';
|
|
146
|
+
repetition: {
|
|
147
|
+
index: number;
|
|
148
|
+
total: number;
|
|
149
|
+
};
|
|
150
|
+
retry_policy: {
|
|
151
|
+
default_max_retries: number;
|
|
152
|
+
backoff: 'exponential';
|
|
153
|
+
initial_delay_ms: number;
|
|
154
|
+
};
|
|
155
|
+
timeout?: {
|
|
156
|
+
default_ms: number;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
100
160
|
/**
|
|
101
161
|
* Individual test case result
|
|
102
162
|
*/
|
|
@@ -108,6 +168,8 @@ export interface CaseResult {
|
|
|
108
168
|
status?: CaseEvaluationStatus;
|
|
109
169
|
/** Number of execution attempts represented by this terminal result. */
|
|
110
170
|
attempts?: number;
|
|
171
|
+
/** Bounded retry-chain evidence for this terminal case result. */
|
|
172
|
+
attempt_evidence?: CaseAttemptEvidence[];
|
|
111
173
|
score: number;
|
|
112
174
|
matcherType: string;
|
|
113
175
|
reason?: string;
|
|
@@ -124,6 +186,8 @@ export interface CaseResult {
|
|
|
124
186
|
error?: string;
|
|
125
187
|
/** Sanitized evaluator evidence; arbitrary evaluator details are never stored here. */
|
|
126
188
|
evidence?: CaseEvaluationEvidence;
|
|
189
|
+
/** Requested and observed target identity for this case. */
|
|
190
|
+
target?: CaseTargetEvidence;
|
|
127
191
|
/** Redaction information for this case */
|
|
128
192
|
redaction?: CaseRedactionInfo;
|
|
129
193
|
/** Ordered tool activity captured for an enabled tool loop. */
|
|
@@ -151,6 +215,28 @@ export interface CostEstimateInfo {
|
|
|
151
215
|
};
|
|
152
216
|
}
|
|
153
217
|
|
|
218
|
+
/** Whether a monetary value is attested, supplied by an operator, or unavailable. */
|
|
219
|
+
export type CostProvenanceStatus = 'known' | 'user_supplied' | 'unavailable';
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Cost evidence suitable for assurance reporting. Generic token-price estimates
|
|
223
|
+
* are intentionally not cost evidence.
|
|
224
|
+
*/
|
|
225
|
+
export interface CostProvenance {
|
|
226
|
+
schema_version: '1';
|
|
227
|
+
status: CostProvenanceStatus;
|
|
228
|
+
/** Required for known and user-supplied amounts. */
|
|
229
|
+
amount?: number;
|
|
230
|
+
/** ISO 4217 currency required with an amount. */
|
|
231
|
+
currency?: string;
|
|
232
|
+
/** Origin of a recorded monetary value. */
|
|
233
|
+
source?: 'provider_billing' | 'operator_input';
|
|
234
|
+
/** ISO timestamp for a recorded monetary value. */
|
|
235
|
+
recorded_at?: string;
|
|
236
|
+
/** Stable reason code when no attested amount is available. */
|
|
237
|
+
unavailable_reason?: 'provider_billing_not_recorded' | 'unsupported_provider' | 'not_requested';
|
|
238
|
+
}
|
|
239
|
+
|
|
154
240
|
/**
|
|
155
241
|
* Run metrics
|
|
156
242
|
*/
|
|
@@ -173,7 +259,10 @@ export interface RunMetrics {
|
|
|
173
259
|
total_prompt_tokens: number;
|
|
174
260
|
total_completion_tokens: number;
|
|
175
261
|
/** Estimated cost information */
|
|
262
|
+
/** @deprecated Generic pricing estimates are not assurance cost evidence. */
|
|
176
263
|
cost?: CostEstimateInfo;
|
|
264
|
+
/** Explicit monetary evidence for assurance reporting. */
|
|
265
|
+
cost_provenance?: CostProvenance;
|
|
177
266
|
}
|
|
178
267
|
|
|
179
268
|
/**
|
|
@@ -302,6 +391,10 @@ export interface RunManifest {
|
|
|
302
391
|
resolved_config?: ResolvedConfig;
|
|
303
392
|
/** Versioned identities for the declared workload and evaluation rubric. */
|
|
304
393
|
workload_identity?: WorkloadIdentity;
|
|
394
|
+
/** Requested and observed target/evaluator configuration for this run. */
|
|
395
|
+
execution_provenance?: ExecutionProvenance;
|
|
396
|
+
/** Retry-chain and repetition context. Present in manifest v1.4+. */
|
|
397
|
+
attempt_evidence?: RunAttemptEvidence;
|
|
305
398
|
metrics: RunMetrics;
|
|
306
399
|
git: GitInfo;
|
|
307
400
|
provenance: ProvenanceInfo;
|
|
@@ -351,6 +444,15 @@ export function assertRunManifestIntegrity(manifest: unknown): asserts manifest
|
|
|
351
444
|
if (manifest.workload_identity !== undefined) {
|
|
352
445
|
assertWorkloadIdentity(manifest.workload_identity);
|
|
353
446
|
}
|
|
447
|
+
if (manifest.execution_provenance !== undefined) {
|
|
448
|
+
assertExecutionProvenance(manifest.execution_provenance);
|
|
449
|
+
}
|
|
450
|
+
if (manifest.attempt_evidence !== undefined) {
|
|
451
|
+
assertRunAttemptEvidence(manifest.attempt_evidence);
|
|
452
|
+
}
|
|
453
|
+
if (isRecord(manifest.metrics) && manifest.metrics.cost_provenance !== undefined) {
|
|
454
|
+
assertCostProvenance(manifest.metrics.cost_provenance);
|
|
455
|
+
}
|
|
354
456
|
|
|
355
457
|
for (const [index, caseResult] of manifest.cases.entries()) {
|
|
356
458
|
if (!isRecord(caseResult)) {
|
|
@@ -370,9 +472,169 @@ export function assertRunManifestIntegrity(manifest: unknown): asserts manifest
|
|
|
370
472
|
if (caseResult.evidence !== undefined) {
|
|
371
473
|
assertCaseEvaluationEvidence(caseResult.evidence, index);
|
|
372
474
|
}
|
|
475
|
+
if (caseResult.target !== undefined) {
|
|
476
|
+
assertCaseTargetEvidence(caseResult.target);
|
|
477
|
+
}
|
|
478
|
+
if (caseResult.attempt_evidence !== undefined) {
|
|
479
|
+
assertCaseAttemptEvidence(caseResult.attempt_evidence, index);
|
|
480
|
+
}
|
|
373
481
|
}
|
|
374
482
|
}
|
|
375
483
|
|
|
484
|
+
function assertRunAttemptEvidence(evidence: unknown): void {
|
|
485
|
+
if (
|
|
486
|
+
!isRecord(evidence) ||
|
|
487
|
+
evidence.schema_version !== '1' ||
|
|
488
|
+
!isRecord(evidence.repetition) ||
|
|
489
|
+
!isPositiveSafeInteger(evidence.repetition.index) ||
|
|
490
|
+
!isPositiveSafeInteger(evidence.repetition.total) ||
|
|
491
|
+
evidence.repetition.index > evidence.repetition.total ||
|
|
492
|
+
!isRecord(evidence.retry_policy) ||
|
|
493
|
+
!isNonnegativeSafeInteger(evidence.retry_policy.default_max_retries) ||
|
|
494
|
+
evidence.retry_policy.backoff !== 'exponential' ||
|
|
495
|
+
!isNonnegativeFiniteNumber(evidence.retry_policy.initial_delay_ms) ||
|
|
496
|
+
(evidence.timeout !== undefined &&
|
|
497
|
+
(!isRecord(evidence.timeout) || !isPositiveFiniteNumber(evidence.timeout.default_ms)))
|
|
498
|
+
) {
|
|
499
|
+
throw new Error('Invalid run manifest: malformed attempt evidence');
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function assertCaseAttemptEvidence(evidence: unknown, index: number): void {
|
|
504
|
+
if (!Array.isArray(evidence) || evidence.length === 0 || evidence.length > 100) {
|
|
505
|
+
throw new Error(`Invalid run manifest: case ${index} has malformed attempt evidence`);
|
|
506
|
+
}
|
|
507
|
+
for (const attempt of evidence) {
|
|
508
|
+
if (
|
|
509
|
+
!isRecord(attempt) ||
|
|
510
|
+
!isBoundedNonemptyString(attempt.attempt_id, 200) ||
|
|
511
|
+
!isBoundedNonemptyString(attempt.retry_chain_id, 200) ||
|
|
512
|
+
!isPositiveSafeInteger(attempt.repetition_index) ||
|
|
513
|
+
!isPositiveSafeInteger(attempt.attempt_number) ||
|
|
514
|
+
!isCaseEvaluationStatus(attempt.status) ||
|
|
515
|
+
typeof attempt.included_in_outcome !== 'boolean' ||
|
|
516
|
+
!isNonnegativeFiniteNumber(attempt.latency_ms) ||
|
|
517
|
+
(attempt.error_code !== undefined &&
|
|
518
|
+
attempt.error_code !== 'timeout' &&
|
|
519
|
+
attempt.error_code !== 'target_error' &&
|
|
520
|
+
attempt.error_code !== 'tool_error')
|
|
521
|
+
) {
|
|
522
|
+
throw new Error(`Invalid run manifest: case ${index} has malformed attempt evidence`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function assertCostProvenance(cost: unknown): void {
|
|
528
|
+
if (!isRecord(cost) || cost.schema_version !== '1') {
|
|
529
|
+
throw new Error('Invalid run manifest: malformed cost provenance');
|
|
530
|
+
}
|
|
531
|
+
if (cost.status === 'unavailable') {
|
|
532
|
+
if (
|
|
533
|
+
cost.amount !== undefined ||
|
|
534
|
+
cost.currency !== undefined ||
|
|
535
|
+
cost.source !== undefined ||
|
|
536
|
+
cost.recorded_at !== undefined ||
|
|
537
|
+
(cost.unavailable_reason !== 'provider_billing_not_recorded' &&
|
|
538
|
+
cost.unavailable_reason !== 'unsupported_provider' &&
|
|
539
|
+
cost.unavailable_reason !== 'not_requested')
|
|
540
|
+
) {
|
|
541
|
+
throw new Error('Invalid run manifest: malformed cost provenance');
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (
|
|
546
|
+
(cost.status !== 'known' && cost.status !== 'user_supplied') ||
|
|
547
|
+
!isNonnegativeFiniteNumber(cost.amount) ||
|
|
548
|
+
!isBoundedNonemptyString(cost.currency, 3) ||
|
|
549
|
+
(cost.status === 'known' && cost.source !== 'provider_billing') ||
|
|
550
|
+
(cost.status === 'user_supplied' && cost.source !== 'operator_input') ||
|
|
551
|
+
!isIsoTimestamp(cost.recorded_at) ||
|
|
552
|
+
cost.unavailable_reason !== undefined
|
|
553
|
+
) {
|
|
554
|
+
throw new Error('Invalid run manifest: malformed cost provenance');
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function isCaseEvaluationStatus(value: unknown): value is CaseEvaluationStatus {
|
|
559
|
+
return value === 'passed' || value === 'failed' || value === 'invalid' || value === 'error';
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function isBoundedNonemptyString(value: unknown, maxLength: number): value is string {
|
|
563
|
+
return typeof value === 'string' && value.length > 0 && value.length <= maxLength;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function isNonnegativeFiniteNumber(value: unknown): value is number {
|
|
567
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function isPositiveFiniteNumber(value: unknown): value is number {
|
|
571
|
+
return isNonnegativeFiniteNumber(value) && value > 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function isNonnegativeSafeInteger(value: unknown): value is number {
|
|
575
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function isPositiveSafeInteger(value: unknown): value is number {
|
|
579
|
+
return isNonnegativeSafeInteger(value) && value > 0;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function isIsoTimestamp(value: unknown): value is string {
|
|
583
|
+
return typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function assertCaseTargetEvidence(target: unknown): void {
|
|
587
|
+
if (
|
|
588
|
+
!isRecord(target) ||
|
|
589
|
+
typeof target.provider !== 'string' ||
|
|
590
|
+
target.provider.length === 0 ||
|
|
591
|
+
target.provider.length > 100 ||
|
|
592
|
+
(target.requested_model !== undefined &&
|
|
593
|
+
(typeof target.requested_model !== 'string' || target.requested_model.length > 200)) ||
|
|
594
|
+
!isBoundedStringList(target.observed_models)
|
|
595
|
+
) {
|
|
596
|
+
throw new Error('Invalid run manifest: malformed target evidence');
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function assertExecutionProvenance(provenance: unknown): void {
|
|
601
|
+
if (!isRecord(provenance) || provenance.schema_version !== '1' || !isRecord(provenance.target)) {
|
|
602
|
+
throw new Error('Invalid run manifest: malformed execution provenance');
|
|
603
|
+
}
|
|
604
|
+
const target = provenance.target;
|
|
605
|
+
if (
|
|
606
|
+
typeof target.provider !== 'string' ||
|
|
607
|
+
target.provider.length === 0 ||
|
|
608
|
+
target.provider.length > 100 ||
|
|
609
|
+
!isBoundedStringList(target.requested_models) ||
|
|
610
|
+
!isBoundedStringList(target.observed_models) ||
|
|
611
|
+
(target.generation !== undefined && !isGenerationConfig(target.generation))
|
|
612
|
+
) {
|
|
613
|
+
throw new Error('Invalid run manifest: malformed execution provenance');
|
|
614
|
+
}
|
|
615
|
+
if (provenance.evaluator !== undefined) {
|
|
616
|
+
if (!isRecord(provenance.evaluator) || !isBoundedStringList(provenance.evaluator.models)) {
|
|
617
|
+
throw new Error('Invalid run manifest: malformed execution provenance');
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function isBoundedStringList(value: unknown): boolean {
|
|
623
|
+
return (
|
|
624
|
+
value === undefined ||
|
|
625
|
+
(Array.isArray(value) &&
|
|
626
|
+
value.length <= 100 &&
|
|
627
|
+
value.every((item) => typeof item === 'string' && item.length > 0 && item.length <= 200))
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function isGenerationConfig(value: unknown): boolean {
|
|
632
|
+
if (!isRecord(value)) return false;
|
|
633
|
+
return [value.temperature, value.max_tokens, value.seed].every(
|
|
634
|
+
(item) => item === undefined || (typeof item === 'number' && Number.isFinite(item))
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
376
638
|
function assertWorkloadIdentity(identity: unknown): void {
|
|
377
639
|
if (
|
|
378
640
|
!isRecord(identity) ||
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { createExecutionProvenance } from './execution-provenance';
|
|
3
|
+
|
|
4
|
+
describe('createExecutionProvenance', () => {
|
|
5
|
+
test('keeps requested, observed target, and evaluator model identities separate', () => {
|
|
6
|
+
const provenance = createExecutionProvenance({
|
|
7
|
+
provider: 'openai',
|
|
8
|
+
requestedModel: 'gpt-requested',
|
|
9
|
+
temperature: 0.2,
|
|
10
|
+
maxTokens: 100,
|
|
11
|
+
seed: 7,
|
|
12
|
+
cases: [
|
|
13
|
+
{
|
|
14
|
+
id: 'one',
|
|
15
|
+
ok: true,
|
|
16
|
+
score: 1,
|
|
17
|
+
matcherType: 'exact',
|
|
18
|
+
latencyMs: 1,
|
|
19
|
+
tokens: { prompt: 1, completion: 1, total: 2 },
|
|
20
|
+
prompt: 'prompt',
|
|
21
|
+
response: 'response',
|
|
22
|
+
expected: { type: 'exact', value: 'response', caseSensitive: true },
|
|
23
|
+
tags: [],
|
|
24
|
+
target: {
|
|
25
|
+
provider: 'openai',
|
|
26
|
+
requested_model: 'gpt-requested',
|
|
27
|
+
observed_models: ['gpt-observed'],
|
|
28
|
+
},
|
|
29
|
+
evidence: { evaluator: 'llm_grader', model: 'judge-model' },
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(provenance).toEqual({
|
|
35
|
+
schema_version: '1',
|
|
36
|
+
target: {
|
|
37
|
+
provider: 'openai',
|
|
38
|
+
requested_models: ['gpt-requested'],
|
|
39
|
+
observed_models: ['gpt-observed'],
|
|
40
|
+
generation: { temperature: 0.2, max_tokens: 100, seed: 7 },
|
|
41
|
+
},
|
|
42
|
+
evaluator: { models: ['judge-model'] },
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('records an unavailable observed identity without inventing one', () => {
|
|
47
|
+
const provenance = createExecutionProvenance({
|
|
48
|
+
provider: 'custom',
|
|
49
|
+
requestedModel: 'requested-model',
|
|
50
|
+
cases: [],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(provenance.target).toEqual({
|
|
54
|
+
provider: 'custom',
|
|
55
|
+
requested_models: ['requested-model'],
|
|
56
|
+
});
|
|
57
|
+
expect(provenance.evaluator).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Bounded execution provenance derived from declared and observed run evidence. */
|
|
2
|
+
|
|
3
|
+
import type { CaseResult, ExecutionProvenance } from '../artifacts/types';
|
|
4
|
+
|
|
5
|
+
export function createExecutionProvenance(options: {
|
|
6
|
+
provider: string;
|
|
7
|
+
requestedModel?: string;
|
|
8
|
+
temperature?: number;
|
|
9
|
+
maxTokens?: number;
|
|
10
|
+
seed?: number;
|
|
11
|
+
cases: CaseResult[];
|
|
12
|
+
}): ExecutionProvenance {
|
|
13
|
+
const requestedModels = uniqueStrings([
|
|
14
|
+
options.requestedModel,
|
|
15
|
+
...options.cases.map((caseResult) => caseResult.target?.requested_model),
|
|
16
|
+
]);
|
|
17
|
+
const observedModels = uniqueStrings(
|
|
18
|
+
options.cases.flatMap((caseResult) => caseResult.target?.observed_models ?? [])
|
|
19
|
+
);
|
|
20
|
+
const evaluatorModels = uniqueStrings(
|
|
21
|
+
options.cases.map((caseResult) => caseResult.evidence?.model)
|
|
22
|
+
);
|
|
23
|
+
const generation = omitUndefined({
|
|
24
|
+
temperature: options.temperature,
|
|
25
|
+
max_tokens: options.maxTokens,
|
|
26
|
+
seed: options.seed,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
schema_version: '1',
|
|
31
|
+
target: {
|
|
32
|
+
provider: boundedString(options.provider, 100) ?? 'unknown',
|
|
33
|
+
...(requestedModels.length ? { requested_models: requestedModels } : {}),
|
|
34
|
+
...(observedModels.length ? { observed_models: observedModels } : {}),
|
|
35
|
+
...(Object.keys(generation).length ? { generation } : {}),
|
|
36
|
+
},
|
|
37
|
+
...(evaluatorModels.length ? { evaluator: { models: evaluatorModels } } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function uniqueStrings(values: unknown[]): string[] {
|
|
42
|
+
return [...new Set(values.map((value) => boundedString(value, 200)).filter(Boolean))] as string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function boundedString(value: unknown, maxLength: number): string | undefined {
|
|
46
|
+
return typeof value === 'string' && value.length > 0 ? value.slice(0, maxLength) : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function omitUndefined<T extends Record<string, number | undefined>>(value: T): Partial<T> {
|
|
50
|
+
return Object.fromEntries(
|
|
51
|
+
Object.entries(value).filter(([, item]) => item !== undefined)
|
|
52
|
+
) as Partial<T>;
|
|
53
|
+
}
|
package/src/provenance/index.ts
CHANGED
|
@@ -170,6 +170,11 @@ describe('executeCase tool loop', () => {
|
|
|
170
170
|
expect(result.error).toBe('TOOL_EXECUTOR_REQUIRED');
|
|
171
171
|
expect(result.latencyMs).toBe(4);
|
|
172
172
|
expect(result.tokens).toEqual({ prompt: 7, completion: 2, total: 9 });
|
|
173
|
+
expect(result.target).toEqual({
|
|
174
|
+
provider: 'ling',
|
|
175
|
+
requested_model: 'Ling-3.0-flash',
|
|
176
|
+
observed_models: ['Ling-3.0-flash'],
|
|
177
|
+
});
|
|
173
178
|
});
|
|
174
179
|
|
|
175
180
|
it('retains prior generation metrics when a later generation rejects', async () => {
|
|
@@ -201,6 +206,7 @@ describe('executeCase tool loop', () => {
|
|
|
201
206
|
steps: 1,
|
|
202
207
|
terminationReason: 'tool_error',
|
|
203
208
|
});
|
|
209
|
+
expect(result.target?.observed_models).toEqual(['Ling-3.0-flash']);
|
|
204
210
|
});
|
|
205
211
|
|
|
206
212
|
it('retains prior generation metrics when a later generation times out', async () => {
|
|
@@ -304,7 +310,61 @@ describe('executeCase measurement integrity', () => {
|
|
|
304
310
|
status: 'error',
|
|
305
311
|
response: '',
|
|
306
312
|
error: 'provider unavailable',
|
|
313
|
+
target: {
|
|
314
|
+
provider: 'test',
|
|
315
|
+
},
|
|
307
316
|
});
|
|
317
|
+
expect(result.target?.observed_models).toBeUndefined();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('records retried target failures as excluded retry-chain attempts', async () => {
|
|
321
|
+
let calls = 0;
|
|
322
|
+
const retryClient: ModelClient = {
|
|
323
|
+
...client,
|
|
324
|
+
generate: async () => {
|
|
325
|
+
calls++;
|
|
326
|
+
if (calls === 1) throw new Error('temporary provider failure');
|
|
327
|
+
return {
|
|
328
|
+
id: 'response',
|
|
329
|
+
model: 'target-model',
|
|
330
|
+
text: 'target response',
|
|
331
|
+
tokens: { prompt: 1, completion: 1, total: 2 },
|
|
332
|
+
latencyMs: 1,
|
|
333
|
+
finishReason: 'stop',
|
|
334
|
+
};
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
registerEvaluator('custom', {
|
|
338
|
+
type: 'custom',
|
|
339
|
+
evaluate: async () => ({ passed: true, score: 1 }),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const result = await executeCase(scenario.cases[0], {
|
|
343
|
+
client: retryClient,
|
|
344
|
+
scenario,
|
|
345
|
+
retries: 1,
|
|
346
|
+
runId: 'assurance-run',
|
|
347
|
+
repetition: { index: 2, total: 3 },
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(result.attempts).toBe(2);
|
|
351
|
+
expect(result.attempt_evidence).toEqual([
|
|
352
|
+
expect.objectContaining({
|
|
353
|
+
attempt_id: 'assurance-run:custom-evaluation:1',
|
|
354
|
+
retry_chain_id: 'assurance-run:custom-evaluation',
|
|
355
|
+
repetition_index: 2,
|
|
356
|
+
attempt_number: 1,
|
|
357
|
+
status: 'error',
|
|
358
|
+
included_in_outcome: false,
|
|
359
|
+
error_code: 'target_error',
|
|
360
|
+
}),
|
|
361
|
+
expect.objectContaining({
|
|
362
|
+
attempt_id: 'assurance-run:custom-evaluation:2',
|
|
363
|
+
attempt_number: 2,
|
|
364
|
+
status: 'passed',
|
|
365
|
+
included_in_outcome: true,
|
|
366
|
+
}),
|
|
367
|
+
]);
|
|
308
368
|
});
|
|
309
369
|
|
|
310
370
|
it('retains only the bounded evidence contract rather than evaluator details', async () => {
|