@artemiskit/core 0.3.0 → 0.4.1
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 +17 -0
- package/README.md +4 -0
- package/dist/adapters/registry.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +20 -2
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/agent-evaluation/index.d.ts +3 -0
- package/dist/agent-evaluation/index.d.ts.map +1 -0
- package/dist/agent-evaluation/scorer.d.ts +35 -0
- package/dist/agent-evaluation/scorer.d.ts.map +1 -0
- package/dist/agent-evaluation/types.d.ts +37 -0
- package/dist/agent-evaluation/types.d.ts.map +1 -0
- package/dist/artifacts/manifest.d.ts.map +1 -1
- package/dist/artifacts/types.d.ts +52 -0
- package/dist/artifacts/types.d.ts.map +1 -1
- package/dist/evaluators/index.d.ts +1 -0
- package/dist/evaluators/index.d.ts.map +1 -1
- package/dist/evaluators/json-schema.d.ts +0 -1
- package/dist/evaluators/json-schema.d.ts.map +1 -1
- package/dist/evaluators/llm-grader.d.ts +2 -0
- package/dist/evaluators/llm-grader.d.ts.map +1 -1
- package/dist/evaluators/tool-trace.d.ts +7 -0
- package/dist/evaluators/tool-trace.d.ts.map +1 -0
- package/dist/evaluators/types.d.ts +20 -0
- package/dist/evaluators/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20281 -13144
- package/dist/runner/executor.d.ts.map +1 -1
- package/dist/runner/runner.d.ts.map +1 -1
- package/dist/runner/types.d.ts +4 -0
- package/dist/runner/types.d.ts.map +1 -1
- package/dist/scenario/schema.d.ts +721 -63
- package/dist/scenario/schema.d.ts.map +1 -1
- package/dist/storage/local.d.ts +1 -1
- package/dist/storage/local.d.ts.map +1 -1
- package/dist/storage/supabase.d.ts +1 -1
- package/dist/storage/supabase.d.ts.map +1 -1
- package/dist/storage/types.d.ts +6 -2
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/tools/fixture-executor.d.ts +10 -0
- package/dist/tools/fixture-executor.d.ts.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/schema-validator.d.ts +10 -0
- package/dist/tools/schema-validator.d.ts.map +1 -0
- package/dist/tools/types.d.ts +50 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/types.test.ts +21 -0
- package/src/adapters/types.ts +18 -0
- package/src/agent-evaluation/index.ts +2 -0
- package/src/agent-evaluation/scorer.test.ts +1194 -0
- package/src/agent-evaluation/scorer.ts +640 -0
- package/src/agent-evaluation/types.test.ts +27 -0
- package/src/agent-evaluation/types.ts +43 -0
- package/src/artifacts/manifest.test.ts +90 -19
- package/src/artifacts/manifest.ts +18 -5
- package/src/artifacts/types.ts +133 -0
- package/src/evaluators/index.ts +3 -0
- package/src/evaluators/json-schema.test.ts +130 -0
- package/src/evaluators/json-schema.ts +38 -63
- package/src/evaluators/llm-grader.test.ts +80 -0
- package/src/evaluators/llm-grader.ts +44 -6
- package/src/evaluators/tool-trace.test.ts +46 -0
- package/src/evaluators/tool-trace.ts +50 -0
- package/src/evaluators/types.ts +20 -0
- package/src/index.ts +6 -0
- package/src/runner/executor.test.ts +374 -0
- package/src/runner/executor.ts +349 -22
- package/src/runner/release-validation.test.ts +169 -0
- package/src/runner/runner.ts +7 -1
- package/src/runner/types.ts +4 -0
- package/src/scenario/schema.ts +56 -1
- package/src/storage/local.test.ts +24 -0
- package/src/storage/local.ts +13 -2
- package/src/storage/supabase.test.ts +111 -1
- package/src/storage/supabase.ts +26 -3
- package/src/storage/types.ts +12 -2
- package/src/tools/fixture-executor.test.ts +88 -0
- package/src/tools/fixture-executor.ts +112 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/schema-validator.test.ts +32 -0
- package/src/tools/schema-validator.ts +56 -0
- package/src/tools/types.ts +80 -0
- package/adapters/openai/dist/index.js +0 -5626
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import { type AgentOutcome, type AgentTask, actionBudgetExceeded } from './types';
|
|
2
|
+
|
|
3
|
+
export type AgentTerminationStatus =
|
|
4
|
+
| 'completed'
|
|
5
|
+
| 'agent_error'
|
|
6
|
+
| 'tool_error'
|
|
7
|
+
| 'infrastructure_error'
|
|
8
|
+
| 'timed_out';
|
|
9
|
+
|
|
10
|
+
export interface AgentTerminationEvidence {
|
|
11
|
+
status: AgentTerminationStatus;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AgentAcceptanceCheck {
|
|
15
|
+
command: string;
|
|
16
|
+
status: 'passed' | 'failed' | 'executor_error';
|
|
17
|
+
exitCode: number | null;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AgentArtifactCheck {
|
|
22
|
+
id: string;
|
|
23
|
+
status: 'passed' | 'failed' | 'executor_error';
|
|
24
|
+
durationMs: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AgentEvaluationEvidence {
|
|
28
|
+
termination: AgentTerminationEvidence;
|
|
29
|
+
acceptanceChecks: AgentAcceptanceCheck[];
|
|
30
|
+
artifactChecks?: AgentArtifactCheck[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type AgentEvaluationVerdict =
|
|
34
|
+
| 'passed'
|
|
35
|
+
| 'passed_with_recovery'
|
|
36
|
+
| 'task_failed'
|
|
37
|
+
| 'infrastructure_failed';
|
|
38
|
+
|
|
39
|
+
export interface AgentEvaluationIssue {
|
|
40
|
+
code: string;
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AgentEvaluationScore {
|
|
45
|
+
taskId: string;
|
|
46
|
+
verdict: AgentEvaluationVerdict;
|
|
47
|
+
passed: boolean;
|
|
48
|
+
recoveredActionCount: number;
|
|
49
|
+
issues: AgentEvaluationIssue[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const TERMINATION_STATUSES = new Set<unknown>([
|
|
53
|
+
'completed',
|
|
54
|
+
'agent_error',
|
|
55
|
+
'tool_error',
|
|
56
|
+
'infrastructure_error',
|
|
57
|
+
'timed_out',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const CHECK_STATUSES = new Set<unknown>(['passed', 'failed', 'executor_error']);
|
|
61
|
+
const ACTION_TYPES = new Set<unknown>(['tool', 'command', 'file']);
|
|
62
|
+
const ACTION_STATUSES = new Set<unknown>(['success', 'error', 'rejected']);
|
|
63
|
+
|
|
64
|
+
type RuntimeRecord = Record<string, unknown>;
|
|
65
|
+
|
|
66
|
+
interface ValidatedScoreInputs {
|
|
67
|
+
startedAtMs: number;
|
|
68
|
+
completedAtMs: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isRuntimeRecord(value: unknown): value is RuntimeRecord {
|
|
72
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isDenseArray(value: unknown): value is unknown[] {
|
|
76
|
+
if (!Array.isArray(value)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
81
|
+
if (!Object.prototype.hasOwnProperty.call(value, index)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isDenseStringArray(value: unknown): value is string[] {
|
|
90
|
+
return isDenseArray(value) && value.every((item) => typeof item === 'string');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isFiniteNonnegativeNumber(value: unknown): value is number {
|
|
94
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function invalidEvidenceScore(task: unknown, code: string, message: string): AgentEvaluationScore {
|
|
98
|
+
return {
|
|
99
|
+
taskId: isRuntimeRecord(task) && typeof task.id === 'string' ? task.id : '',
|
|
100
|
+
verdict: 'infrastructure_failed',
|
|
101
|
+
passed: false,
|
|
102
|
+
recoveredActionCount: 0,
|
|
103
|
+
issues: [{ code, message }],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeRelativePath(path: string): string | undefined {
|
|
108
|
+
const slashPath = path.replaceAll('\\', '/');
|
|
109
|
+
if (slashPath.startsWith('/') || /^[A-Za-z]:\//.test(slashPath) || slashPath.includes('\0')) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const segments = slashPath.split('/').filter((segment) => segment !== '' && segment !== '.');
|
|
114
|
+
if (segments.length === 0 || segments.includes('..')) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return segments.join('/');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isAllowedChangedPath(changedPath: string, allowedPaths: string[]): boolean {
|
|
122
|
+
const normalizedChangedPath = normalizeRelativePath(changedPath);
|
|
123
|
+
if (!normalizedChangedPath) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return allowedPaths.some((allowedPath) => {
|
|
128
|
+
const normalizedAllowedPath = normalizeRelativePath(allowedPath);
|
|
129
|
+
return (
|
|
130
|
+
normalizedAllowedPath !== undefined &&
|
|
131
|
+
(normalizedChangedPath === normalizedAllowedPath ||
|
|
132
|
+
normalizedChangedPath.startsWith(`${normalizedAllowedPath}/`))
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateScoreInputs(
|
|
138
|
+
task: unknown,
|
|
139
|
+
outcome: unknown,
|
|
140
|
+
evidence: unknown
|
|
141
|
+
): AgentEvaluationScore | ValidatedScoreInputs {
|
|
142
|
+
if (
|
|
143
|
+
!isRuntimeRecord(task) ||
|
|
144
|
+
typeof task.id !== 'string' ||
|
|
145
|
+
typeof task.fixturePath !== 'string' ||
|
|
146
|
+
!isDenseStringArray(task.allowedPaths) ||
|
|
147
|
+
task.allowedPaths.some((path) => normalizeRelativePath(path) === undefined) ||
|
|
148
|
+
!isDenseStringArray(task.allowedTools) ||
|
|
149
|
+
!isDenseStringArray(task.acceptanceCommands) ||
|
|
150
|
+
(task.requiredArtifactChecks !== undefined &&
|
|
151
|
+
!isDenseStringArray(task.requiredArtifactChecks)) ||
|
|
152
|
+
!isFiniteNonnegativeNumber(task.maxActions) ||
|
|
153
|
+
!Number.isInteger(task.maxActions) ||
|
|
154
|
+
!isFiniteNonnegativeNumber(task.timeoutMs)
|
|
155
|
+
) {
|
|
156
|
+
return invalidEvidenceScore(
|
|
157
|
+
task,
|
|
158
|
+
'task-definition-invalid',
|
|
159
|
+
'Task definition is malformed or contains invalid runtime values.'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
!isRuntimeRecord(outcome) ||
|
|
165
|
+
typeof outcome.taskId !== 'string' ||
|
|
166
|
+
typeof outcome.completed !== 'boolean' ||
|
|
167
|
+
typeof outcome.acceptancePassed !== 'boolean' ||
|
|
168
|
+
!isRuntimeRecord(outcome.trace) ||
|
|
169
|
+
(outcome.error !== undefined && typeof outcome.error !== 'string')
|
|
170
|
+
) {
|
|
171
|
+
return invalidEvidenceScore(
|
|
172
|
+
task,
|
|
173
|
+
'outcome-evidence-invalid',
|
|
174
|
+
'Outcome evidence is malformed or contains invalid runtime values.'
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const trace = outcome.trace;
|
|
179
|
+
if (
|
|
180
|
+
typeof trace.taskId !== 'string' ||
|
|
181
|
+
!Array.isArray(trace.actions) ||
|
|
182
|
+
!isDenseStringArray(trace.changedPaths)
|
|
183
|
+
) {
|
|
184
|
+
return invalidEvidenceScore(
|
|
185
|
+
task,
|
|
186
|
+
'trace-evidence-invalid',
|
|
187
|
+
'Trace evidence is malformed or contains invalid runtime values.'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (let index = 0; index < trace.actions.length; index += 1) {
|
|
192
|
+
const action = trace.actions[index];
|
|
193
|
+
if (
|
|
194
|
+
!isRuntimeRecord(action) ||
|
|
195
|
+
!ACTION_TYPES.has(action.type) ||
|
|
196
|
+
typeof action.name !== 'string' ||
|
|
197
|
+
!ACTION_STATUSES.has(action.status) ||
|
|
198
|
+
!isFiniteNonnegativeNumber(action.durationMs) ||
|
|
199
|
+
(action.summary !== undefined && typeof action.summary !== 'string')
|
|
200
|
+
) {
|
|
201
|
+
return invalidEvidenceScore(
|
|
202
|
+
task,
|
|
203
|
+
'action-evidence-invalid',
|
|
204
|
+
`Action evidence is invalid at index ${index}.`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (typeof trace.startedAt !== 'string' || typeof trace.completedAt !== 'string') {
|
|
210
|
+
return invalidEvidenceScore(
|
|
211
|
+
task,
|
|
212
|
+
'trace-timestamp-invalid',
|
|
213
|
+
'Trace timestamps are missing, invalid, or out of order.'
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const startedAtMs = Date.parse(trace.startedAt);
|
|
218
|
+
const completedAtMs = Date.parse(trace.completedAt);
|
|
219
|
+
if (
|
|
220
|
+
!Number.isFinite(startedAtMs) ||
|
|
221
|
+
!Number.isFinite(completedAtMs) ||
|
|
222
|
+
completedAtMs < startedAtMs
|
|
223
|
+
) {
|
|
224
|
+
return invalidEvidenceScore(
|
|
225
|
+
task,
|
|
226
|
+
'trace-timestamp-invalid',
|
|
227
|
+
'Trace timestamps are missing, invalid, or out of order.'
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (outcome.finalDiff !== undefined && typeof outcome.finalDiff !== 'string') {
|
|
232
|
+
return invalidEvidenceScore(
|
|
233
|
+
task,
|
|
234
|
+
'final-diff-invalid',
|
|
235
|
+
'Final diff evidence must be a string when present.'
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!isRuntimeRecord(evidence)) {
|
|
240
|
+
return invalidEvidenceScore(
|
|
241
|
+
task,
|
|
242
|
+
'evaluation-evidence-invalid',
|
|
243
|
+
'Evaluation evidence is malformed or contains invalid runtime values.'
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (
|
|
248
|
+
!isRuntimeRecord(evidence.termination) ||
|
|
249
|
+
!TERMINATION_STATUSES.has(evidence.termination.status)
|
|
250
|
+
) {
|
|
251
|
+
return invalidEvidenceScore(
|
|
252
|
+
task,
|
|
253
|
+
'termination-evidence-invalid',
|
|
254
|
+
'Termination evidence contains an unknown or malformed status.'
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!isDenseArray(evidence.acceptanceChecks)) {
|
|
259
|
+
return invalidEvidenceScore(
|
|
260
|
+
task,
|
|
261
|
+
'acceptance-evidence-invalid',
|
|
262
|
+
'Acceptance evidence is malformed or contains invalid runtime values.'
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const check of evidence.acceptanceChecks) {
|
|
267
|
+
if (
|
|
268
|
+
!isRuntimeRecord(check) ||
|
|
269
|
+
typeof check.command !== 'string' ||
|
|
270
|
+
!CHECK_STATUSES.has(check.status) ||
|
|
271
|
+
!isFiniteNonnegativeNumber(check.durationMs) ||
|
|
272
|
+
(check.status === 'passed' && check.exitCode !== 0) ||
|
|
273
|
+
(check.status === 'failed' && (!Number.isInteger(check.exitCode) || check.exitCode === 0)) ||
|
|
274
|
+
(check.status === 'executor_error' && check.exitCode !== null)
|
|
275
|
+
) {
|
|
276
|
+
return invalidEvidenceScore(
|
|
277
|
+
task,
|
|
278
|
+
'acceptance-evidence-invalid',
|
|
279
|
+
'Acceptance evidence is malformed or internally inconsistent.'
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (evidence.artifactChecks !== undefined) {
|
|
285
|
+
if (!isDenseArray(evidence.artifactChecks)) {
|
|
286
|
+
return invalidEvidenceScore(
|
|
287
|
+
task,
|
|
288
|
+
'artifact-evidence-invalid',
|
|
289
|
+
'Artifact evidence is malformed or contains invalid runtime values.'
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const check of evidence.artifactChecks) {
|
|
294
|
+
if (
|
|
295
|
+
!isRuntimeRecord(check) ||
|
|
296
|
+
typeof check.id !== 'string' ||
|
|
297
|
+
!CHECK_STATUSES.has(check.status) ||
|
|
298
|
+
!isFiniteNonnegativeNumber(check.durationMs)
|
|
299
|
+
) {
|
|
300
|
+
return invalidEvidenceScore(
|
|
301
|
+
task,
|
|
302
|
+
'artifact-evidence-invalid',
|
|
303
|
+
'Artifact evidence is malformed or contains invalid runtime values.'
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return { startedAtMs, completedAtMs };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function scoreAgentOutcome(
|
|
313
|
+
task: AgentTask,
|
|
314
|
+
outcome: AgentOutcome,
|
|
315
|
+
evidence: AgentEvaluationEvidence
|
|
316
|
+
): AgentEvaluationScore {
|
|
317
|
+
const validatedInputs = validateScoreInputs(task, outcome, evidence);
|
|
318
|
+
if ('verdict' in validatedInputs) {
|
|
319
|
+
return validatedInputs;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (outcome.taskId !== task.id || outcome.trace.taskId !== task.id) {
|
|
323
|
+
return {
|
|
324
|
+
taskId: task.id,
|
|
325
|
+
verdict: 'infrastructure_failed',
|
|
326
|
+
passed: false,
|
|
327
|
+
recoveredActionCount: 0,
|
|
328
|
+
issues: [
|
|
329
|
+
{
|
|
330
|
+
code: 'task-evidence-mismatch',
|
|
331
|
+
message: 'Outcome or trace evidence belongs to a different task.',
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const recoveredActionCount = outcome.trace.actions.filter(
|
|
338
|
+
(action) => action.status === 'error'
|
|
339
|
+
).length;
|
|
340
|
+
const infrastructureTerminationCodes: Partial<Record<AgentTerminationStatus, string>> = {
|
|
341
|
+
tool_error: 'tool-termination-error',
|
|
342
|
+
infrastructure_error: 'infrastructure-termination-error',
|
|
343
|
+
};
|
|
344
|
+
const infrastructureTerminationCode = infrastructureTerminationCodes[evidence.termination.status];
|
|
345
|
+
|
|
346
|
+
if (infrastructureTerminationCode) {
|
|
347
|
+
return {
|
|
348
|
+
taskId: task.id,
|
|
349
|
+
verdict: 'infrastructure_failed',
|
|
350
|
+
passed: false,
|
|
351
|
+
recoveredActionCount: 0,
|
|
352
|
+
issues: [
|
|
353
|
+
{
|
|
354
|
+
code: infrastructureTerminationCode,
|
|
355
|
+
message: `Run terminated with ${evidence.termination.status}.`,
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const taskTerminationCodes: Partial<Record<AgentTerminationStatus, string>> = {
|
|
362
|
+
agent_error: 'agent-termination-error',
|
|
363
|
+
timed_out: 'agent-timed-out',
|
|
364
|
+
};
|
|
365
|
+
const taskTerminationCode = taskTerminationCodes[evidence.termination.status];
|
|
366
|
+
if (taskTerminationCode) {
|
|
367
|
+
return {
|
|
368
|
+
taskId: task.id,
|
|
369
|
+
verdict: 'task_failed',
|
|
370
|
+
passed: false,
|
|
371
|
+
recoveredActionCount: 0,
|
|
372
|
+
issues: [
|
|
373
|
+
{
|
|
374
|
+
code: taskTerminationCode,
|
|
375
|
+
message: `Run terminated with ${evidence.termination.status}.`,
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!outcome.completed) {
|
|
382
|
+
return {
|
|
383
|
+
taskId: task.id,
|
|
384
|
+
verdict: 'infrastructure_failed',
|
|
385
|
+
passed: false,
|
|
386
|
+
recoveredActionCount: 0,
|
|
387
|
+
issues: [
|
|
388
|
+
{
|
|
389
|
+
code: 'completion-evidence-mismatch',
|
|
390
|
+
message: 'Termination evidence reports completion but the outcome does not.',
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const artifactChecks = evidence.artifactChecks ?? [];
|
|
397
|
+
const artifactExecutorError = artifactChecks.find((check) => check.status === 'executor_error');
|
|
398
|
+
if (artifactExecutorError) {
|
|
399
|
+
return {
|
|
400
|
+
taskId: task.id,
|
|
401
|
+
verdict: 'infrastructure_failed',
|
|
402
|
+
passed: false,
|
|
403
|
+
recoveredActionCount: 0,
|
|
404
|
+
issues: [
|
|
405
|
+
{
|
|
406
|
+
code: 'artifact-executor-error',
|
|
407
|
+
message: `Artifact check could not be executed: ${artifactExecutorError.id}.`,
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
for (const checkId of task.requiredArtifactChecks ?? []) {
|
|
414
|
+
const matchingChecks = artifactChecks.filter((check) => check.id === checkId);
|
|
415
|
+
if (matchingChecks.length === 0) {
|
|
416
|
+
return {
|
|
417
|
+
taskId: task.id,
|
|
418
|
+
verdict: 'infrastructure_failed',
|
|
419
|
+
passed: false,
|
|
420
|
+
recoveredActionCount: 0,
|
|
421
|
+
issues: [
|
|
422
|
+
{
|
|
423
|
+
code: 'artifact-evidence-missing',
|
|
424
|
+
message: `No artifact evidence was recorded for: ${checkId}.`,
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
if (matchingChecks.length > 1) {
|
|
430
|
+
return {
|
|
431
|
+
taskId: task.id,
|
|
432
|
+
verdict: 'infrastructure_failed',
|
|
433
|
+
passed: false,
|
|
434
|
+
recoveredActionCount: 0,
|
|
435
|
+
issues: [
|
|
436
|
+
{
|
|
437
|
+
code: 'artifact-evidence-duplicate',
|
|
438
|
+
message: `Multiple artifact results were recorded for: ${checkId}.`,
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
for (const command of task.acceptanceCommands) {
|
|
446
|
+
const matchingChecks = evidence.acceptanceChecks.filter((check) => check.command === command);
|
|
447
|
+
if (matchingChecks.length === 0) {
|
|
448
|
+
return {
|
|
449
|
+
taskId: task.id,
|
|
450
|
+
verdict: 'infrastructure_failed',
|
|
451
|
+
passed: false,
|
|
452
|
+
recoveredActionCount: 0,
|
|
453
|
+
issues: [
|
|
454
|
+
{
|
|
455
|
+
code: 'acceptance-evidence-missing',
|
|
456
|
+
message: `No acceptance evidence was recorded for: ${command}.`,
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
if (matchingChecks.length > 1) {
|
|
462
|
+
return {
|
|
463
|
+
taskId: task.id,
|
|
464
|
+
verdict: 'infrastructure_failed',
|
|
465
|
+
passed: false,
|
|
466
|
+
recoveredActionCount: 0,
|
|
467
|
+
issues: [
|
|
468
|
+
{
|
|
469
|
+
code: 'acceptance-evidence-duplicate',
|
|
470
|
+
message: `Multiple acceptance results were recorded for: ${command}.`,
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const executorError = evidence.acceptanceChecks.find(
|
|
478
|
+
(check) => check.status === 'executor_error'
|
|
479
|
+
);
|
|
480
|
+
if (executorError) {
|
|
481
|
+
return {
|
|
482
|
+
taskId: task.id,
|
|
483
|
+
verdict: 'infrastructure_failed',
|
|
484
|
+
passed: false,
|
|
485
|
+
recoveredActionCount: 0,
|
|
486
|
+
issues: [
|
|
487
|
+
{
|
|
488
|
+
code: 'acceptance-executor-error',
|
|
489
|
+
message: `Acceptance command could not be executed: ${executorError.command}.`,
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const acceptancePassed = task.acceptanceCommands.every((command) => {
|
|
496
|
+
const check = evidence.acceptanceChecks.find((candidate) => candidate.command === command);
|
|
497
|
+
return check?.status === 'passed' && check.exitCode === 0;
|
|
498
|
+
});
|
|
499
|
+
if (outcome.acceptancePassed !== acceptancePassed) {
|
|
500
|
+
return {
|
|
501
|
+
taskId: task.id,
|
|
502
|
+
verdict: 'infrastructure_failed',
|
|
503
|
+
passed: false,
|
|
504
|
+
recoveredActionCount: 0,
|
|
505
|
+
issues: [
|
|
506
|
+
{
|
|
507
|
+
code: 'acceptance-evidence-mismatch',
|
|
508
|
+
message: 'The outcome acceptance summary does not match the recorded command evidence.',
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (outcome.trace.changedPaths.length > 0 && !outcome.finalDiff?.trim()) {
|
|
515
|
+
return {
|
|
516
|
+
taskId: task.id,
|
|
517
|
+
verdict: 'infrastructure_failed',
|
|
518
|
+
passed: false,
|
|
519
|
+
recoveredActionCount: 0,
|
|
520
|
+
issues: [
|
|
521
|
+
{
|
|
522
|
+
code: 'final-diff-missing',
|
|
523
|
+
message: 'Changed paths were recorded without a non-empty final diff.',
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const failedArtifactCheck = artifactChecks.find(
|
|
530
|
+
(check) => task.requiredArtifactChecks?.includes(check.id) === true && check.status === 'failed'
|
|
531
|
+
);
|
|
532
|
+
if (failedArtifactCheck) {
|
|
533
|
+
return {
|
|
534
|
+
taskId: task.id,
|
|
535
|
+
verdict: 'task_failed',
|
|
536
|
+
passed: false,
|
|
537
|
+
recoveredActionCount: 0,
|
|
538
|
+
issues: [
|
|
539
|
+
{
|
|
540
|
+
code: 'artifact-check-failed',
|
|
541
|
+
message: `Required artifact check failed: ${failedArtifactCheck.id}.`,
|
|
542
|
+
},
|
|
543
|
+
],
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const failedAcceptanceCheck = evidence.acceptanceChecks.find(
|
|
548
|
+
(check) => check.status === 'failed'
|
|
549
|
+
);
|
|
550
|
+
if (failedAcceptanceCheck) {
|
|
551
|
+
return {
|
|
552
|
+
taskId: task.id,
|
|
553
|
+
verdict: 'task_failed',
|
|
554
|
+
passed: false,
|
|
555
|
+
recoveredActionCount: 0,
|
|
556
|
+
issues: [
|
|
557
|
+
{
|
|
558
|
+
code: 'acceptance-failed',
|
|
559
|
+
message: `Acceptance command failed: ${failedAcceptanceCheck.command}.`,
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const disallowedChangedPathIndex = outcome.trace.changedPaths.findIndex(
|
|
566
|
+
(changedPath) => !isAllowedChangedPath(changedPath, task.allowedPaths)
|
|
567
|
+
);
|
|
568
|
+
if (disallowedChangedPathIndex !== -1) {
|
|
569
|
+
const disallowedChangedPath = outcome.trace.changedPaths[disallowedChangedPathIndex];
|
|
570
|
+
return {
|
|
571
|
+
taskId: task.id,
|
|
572
|
+
verdict: 'task_failed',
|
|
573
|
+
passed: false,
|
|
574
|
+
recoveredActionCount: 0,
|
|
575
|
+
issues: [
|
|
576
|
+
{
|
|
577
|
+
code: 'changed-path-violation',
|
|
578
|
+
message: `Changed path is outside the allowed scope: ${disallowedChangedPath}.`,
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const prohibitedAction = outcome.trace.actions.find(
|
|
585
|
+
(action) => action.status === 'rejected' || !task.allowedTools.includes(action.name)
|
|
586
|
+
);
|
|
587
|
+
if (prohibitedAction) {
|
|
588
|
+
return {
|
|
589
|
+
taskId: task.id,
|
|
590
|
+
verdict: 'task_failed',
|
|
591
|
+
passed: false,
|
|
592
|
+
recoveredActionCount: 0,
|
|
593
|
+
issues: [
|
|
594
|
+
{
|
|
595
|
+
code: 'prohibited-action',
|
|
596
|
+
message: `Tool action was prohibited or rejected: ${prohibitedAction.name}.`,
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (actionBudgetExceeded(task, outcome.trace)) {
|
|
603
|
+
return {
|
|
604
|
+
taskId: task.id,
|
|
605
|
+
verdict: 'task_failed',
|
|
606
|
+
passed: false,
|
|
607
|
+
recoveredActionCount: 0,
|
|
608
|
+
issues: [
|
|
609
|
+
{
|
|
610
|
+
code: 'action-budget-exceeded',
|
|
611
|
+
message: `Action budget exceeded: ${outcome.trace.actions.length}/${task.maxActions}.`,
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const elapsedMs = validatedInputs.completedAtMs - validatedInputs.startedAtMs;
|
|
618
|
+
if (elapsedMs > task.timeoutMs) {
|
|
619
|
+
return {
|
|
620
|
+
taskId: task.id,
|
|
621
|
+
verdict: 'task_failed',
|
|
622
|
+
passed: false,
|
|
623
|
+
recoveredActionCount: 0,
|
|
624
|
+
issues: [
|
|
625
|
+
{
|
|
626
|
+
code: 'time-budget-exceeded',
|
|
627
|
+
message: `Time budget exceeded: ${elapsedMs}/${task.timeoutMs}ms.`,
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
taskId: task.id,
|
|
635
|
+
verdict: recoveredActionCount > 0 ? 'passed_with_recovery' : 'passed',
|
|
636
|
+
passed: true,
|
|
637
|
+
recoveredActionCount,
|
|
638
|
+
issues: [],
|
|
639
|
+
};
|
|
640
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { type AgentTask, type AgentTrace, actionBudgetExceeded } from './types';
|
|
3
|
+
|
|
4
|
+
describe('real agent evaluation contracts', () => {
|
|
5
|
+
it('detects action-budget violations', () => {
|
|
6
|
+
const task: AgentTask = {
|
|
7
|
+
id: 'repair',
|
|
8
|
+
fixturePath: '/tmp/fixture',
|
|
9
|
+
allowedPaths: ['scenario.yaml'],
|
|
10
|
+
allowedTools: ['workspace_read'],
|
|
11
|
+
maxActions: 1,
|
|
12
|
+
timeoutMs: 60_000,
|
|
13
|
+
acceptanceCommands: ['akit validate scenario.yaml'],
|
|
14
|
+
};
|
|
15
|
+
const trace: AgentTrace = {
|
|
16
|
+
taskId: 'repair',
|
|
17
|
+
actions: [
|
|
18
|
+
{ type: 'tool', name: 'workspace_read', status: 'success', durationMs: 1 },
|
|
19
|
+
{ type: 'command', name: 'akit validate', status: 'success', durationMs: 1 },
|
|
20
|
+
],
|
|
21
|
+
changedPaths: [],
|
|
22
|
+
startedAt: '2026-08-21T00:00:00Z',
|
|
23
|
+
completedAt: '2026-08-21T00:00:01Z',
|
|
24
|
+
};
|
|
25
|
+
expect(actionBudgetExceeded(task, trace)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface AgentTask {
|
|
2
|
+
id: string;
|
|
3
|
+
fixturePath: string;
|
|
4
|
+
allowedPaths: string[];
|
|
5
|
+
allowedTools: string[];
|
|
6
|
+
maxActions: number;
|
|
7
|
+
timeoutMs: number;
|
|
8
|
+
acceptanceCommands: string[];
|
|
9
|
+
requiredArtifactChecks?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AgentAction {
|
|
13
|
+
type: 'tool' | 'command' | 'file';
|
|
14
|
+
name: string;
|
|
15
|
+
status: 'success' | 'error' | 'rejected';
|
|
16
|
+
durationMs: number;
|
|
17
|
+
summary?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AgentTrace {
|
|
21
|
+
taskId: string;
|
|
22
|
+
actions: AgentAction[];
|
|
23
|
+
changedPaths: string[];
|
|
24
|
+
startedAt: string;
|
|
25
|
+
completedAt: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AgentOutcome {
|
|
29
|
+
taskId: string;
|
|
30
|
+
completed: boolean;
|
|
31
|
+
acceptancePassed: boolean;
|
|
32
|
+
trace: AgentTrace;
|
|
33
|
+
finalDiff?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AgentHarness {
|
|
38
|
+
run(task: AgentTask): Promise<AgentOutcome>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function actionBudgetExceeded(task: AgentTask, trace: AgentTrace): boolean {
|
|
42
|
+
return trace.actions.length > task.maxActions;
|
|
43
|
+
}
|