pi-harness-runtime 0.10.22 → 0.10.24
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 +37 -0
- package/index.ts +25 -5
- package/package.json +2 -2
- package/packages/file-copy-helper/dist/src/extension.js +62 -0
- package/packages/file-copy-helper/src/extension.ts +87 -0
- package/packages/file-copy-helper/src/index.ts +1 -0
- package/packages/todo-bd-sync/dist/src/detector.js +108 -0
- package/packages/todo-bd-sync/src/detector.ts +1 -1
- package/packages/auth/src/minimax-browser-auth.js +0 -889
- package/packages/capability-registry/src/types.js +0 -10
- package/packages/checkpoint/src/checkpoint-manager.js +0 -29
- package/packages/clipboard/src/copy-sync.js +0 -169
- package/packages/clipboard/src/gist-relay.js +0 -185
- package/packages/clipboard/src/github-login.js +0 -68
- package/packages/config-capture/src/index.js +0 -268
- package/packages/cookie-sanitizer/src/atomic-write.js +0 -66
- package/packages/cookie-sanitizer/src/detect-format.js +0 -87
- package/packages/cookie-sanitizer/src/index.js +0 -38
- package/packages/cookie-sanitizer/src/infer-provider.js +0 -95
- package/packages/cookie-sanitizer/src/normalize.js +0 -74
- package/packages/cookie-sanitizer/src/parse-json.js +0 -107
- package/packages/cookie-sanitizer/src/parse-netscape.js +0 -98
- package/packages/cookie-sanitizer/src/sync.js +0 -268
- package/packages/cookie-sanitizer/src/types.js +0 -12
- package/packages/cookie-sanitizer/src/watcher.js +0 -147
- package/packages/evaluation-engine/src/engine.js +0 -343
- package/packages/evaluation-engine/src/index.js +0 -7
- package/packages/evaluation-engine/src/types.js +0 -14
- package/packages/event-store/src/types.js +0 -5
- package/packages/experience-replay/src/index.js +0 -6
- package/packages/experience-replay/src/replay.js +0 -228
- package/packages/experience-replay/src/types.js +0 -6
- package/packages/learning-engine/src/engine.js +0 -294
- package/packages/learning-engine/src/index.js +0 -7
- package/packages/learning-engine/src/types.js +0 -63
- package/packages/memory-engine/src/engine.js +0 -507
- package/packages/memory-engine/src/index.js +0 -7
- package/packages/memory-engine/src/types.js +0 -39
- package/packages/model-registry/src/types.js +0 -5
- package/packages/performance-optimizer/src/index.js +0 -6
- package/packages/performance-optimizer/src/optimizer.js +0 -304
- package/packages/performance-optimizer/src/types.js +0 -6
- package/packages/projection-engine/src/types.js +0 -5
- package/packages/providers/src/provider-id.js +0 -112
- package/packages/quota-manager/src/quota-manager.js +0 -259
- package/packages/quota-manager/src/tui-usage-monitor.js +0 -267
- package/packages/todo-bd-sync/src/detector.js +0 -131
- package/packages/todo-bd-sync/src/extension.js +0 -104
- package/packages/todo-bd-sync/src/sync.js +0 -289
- package/packages/todo-bd-sync/src/task-analyzer.js +0 -239
- package/packages/todo-bd-sync/src/types.js +0 -66
- package/packages/token-estimation/src/index.js +0 -114
- package/packages/workflow-events/src/types.js +0 -5
- package/packages/write-review/src/blackboard.js +0 -228
- package/packages/write-review/src/gate.js +0 -137
- package/packages/write-review/src/index.js +0 -17
- package/packages/write-review/src/injection.js +0 -141
- package/packages/write-review/src/review.js +0 -153
- package/packages/write-review/src/trigger.js +0 -106
- package/packages/write-review/src/types.js +0 -25
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Evaluation Engine (RFC-0057)
|
|
3
|
-
*
|
|
4
|
-
* Determines whether an agent-produced result is acceptable before advancing.
|
|
5
|
-
*/
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { DEFAULT_SCORING_WEIGHTS } from "./types.js";
|
|
8
|
-
// --- Evidence Collection -------------------------------------------------------
|
|
9
|
-
function collectEvidence(outputs, testResults, policyFindings) {
|
|
10
|
-
const evidence = [];
|
|
11
|
-
// Add test results as evidence
|
|
12
|
-
for (const test of testResults) {
|
|
13
|
-
evidence.push({
|
|
14
|
-
kind: "test",
|
|
15
|
-
path: test.testFile,
|
|
16
|
-
description: `Test ${test.id}: ${test.status}`,
|
|
17
|
-
hash: test.id,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
// Add output files as evidence
|
|
21
|
-
for (const output of outputs) {
|
|
22
|
-
if (output.path) {
|
|
23
|
-
evidence.push({
|
|
24
|
-
kind: "file",
|
|
25
|
-
path: output.path,
|
|
26
|
-
description: output.description,
|
|
27
|
-
hash: output.hash,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
// Add policy findings as evidence
|
|
32
|
-
for (const finding of policyFindings) {
|
|
33
|
-
evidence.push({
|
|
34
|
-
kind: "policy",
|
|
35
|
-
path: finding.filePath,
|
|
36
|
-
description: `[${finding.severity.toUpperCase()}] ${finding.ruleId}: ${finding.message}`,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
return evidence;
|
|
40
|
-
}
|
|
41
|
-
// --- Missing Output Detection --------------------------------------------------
|
|
42
|
-
function findMissingOutputs(requiredArtifacts, outputs) {
|
|
43
|
-
const findings = [];
|
|
44
|
-
const outputPaths = new Set(outputs.filter((o) => o.path).map((o) => o.path));
|
|
45
|
-
for (const required of requiredArtifacts) {
|
|
46
|
-
const requiredLower = required.toLowerCase();
|
|
47
|
-
const found = outputPaths.has(required) ||
|
|
48
|
-
outputs.some((o) => o.description.toLowerCase().includes(requiredLower));
|
|
49
|
-
if (!found) {
|
|
50
|
-
findings.push({
|
|
51
|
-
dimension: "correctness",
|
|
52
|
-
severity: "critical",
|
|
53
|
-
message: `Required artifact '${required}' is missing from outputs`,
|
|
54
|
-
evidence: `Required: ${required}, Available: ${[...outputPaths].join(", ") || "none"}`,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return findings;
|
|
59
|
-
}
|
|
60
|
-
// --- Test Analysis ------------------------------------------------------------
|
|
61
|
-
function analyzeTests(testResults) {
|
|
62
|
-
const findings = [];
|
|
63
|
-
let passedCount = 0;
|
|
64
|
-
let totalCount = 0;
|
|
65
|
-
let hasFailures = false;
|
|
66
|
-
for (const test of testResults) {
|
|
67
|
-
totalCount++;
|
|
68
|
-
if (test.status === "passed") {
|
|
69
|
-
passedCount++;
|
|
70
|
-
}
|
|
71
|
-
else if (test.status === "failed" || test.status === "error") {
|
|
72
|
-
hasFailures = true;
|
|
73
|
-
findings.push({
|
|
74
|
-
dimension: "test_completeness",
|
|
75
|
-
severity: "critical",
|
|
76
|
-
message: `Test '${test.testFile}' ${test.status}: ${test.errorMessage || "Unknown error"}`,
|
|
77
|
-
evidence: test.errorMessage,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (testResults.length === 0) {
|
|
82
|
-
findings.push({
|
|
83
|
-
dimension: "test_completeness",
|
|
84
|
-
severity: "high",
|
|
85
|
-
message: "No test results provided",
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
return { findings, passedCount, totalCount, hasFailures };
|
|
89
|
-
}
|
|
90
|
-
// --- Policy Analysis ----------------------------------------------------------
|
|
91
|
-
function analyzePolicies(policyFindings) {
|
|
92
|
-
const findings = [];
|
|
93
|
-
const criticalFindings = [];
|
|
94
|
-
for (const finding of policyFindings) {
|
|
95
|
-
findings.push({
|
|
96
|
-
dimension: "policy_compliance",
|
|
97
|
-
severity: finding.severity,
|
|
98
|
-
message: `${finding.ruleId}: ${finding.message}`,
|
|
99
|
-
evidence: finding.filePath
|
|
100
|
-
? `${finding.filePath}:${finding.line || "?"}`
|
|
101
|
-
: undefined,
|
|
102
|
-
});
|
|
103
|
-
if (finding.severity === "critical") {
|
|
104
|
-
criticalFindings.push(finding);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return { findings, criticalFindings };
|
|
108
|
-
}
|
|
109
|
-
// --- Acceptance Criteria Coverage ---------------------------------------------
|
|
110
|
-
function analyzeAcceptanceCoverage(acceptanceCriteria, outputs, testResults) {
|
|
111
|
-
const findings = [];
|
|
112
|
-
const coveredCriteria = [];
|
|
113
|
-
const unmetCriteria = [];
|
|
114
|
-
const outputText = outputs
|
|
115
|
-
.map((o) => `${o.description} ${o.path || ""}`.toLowerCase())
|
|
116
|
-
.join(" ");
|
|
117
|
-
const passingTests = testResults.filter((t) => t.status === "passed");
|
|
118
|
-
const passingTestText = passingTests
|
|
119
|
-
.map((t) => `${t.testFile} ${t.errorMessage || ""}`.toLowerCase())
|
|
120
|
-
.join(" ");
|
|
121
|
-
for (const criterion of acceptanceCriteria) {
|
|
122
|
-
const criterionLower = criterion.toLowerCase();
|
|
123
|
-
const keywords = criterionLower
|
|
124
|
-
.split(/[^a-z0-9]+/)
|
|
125
|
-
.filter((word) => word.length >= 4);
|
|
126
|
-
const outputKeywordMatches = keywords.filter((word) => outputText.includes(word)).length;
|
|
127
|
-
const coveredByOutput = keywords.length > 0 &&
|
|
128
|
-
outputKeywordMatches >= Math.ceil(keywords.length / 2);
|
|
129
|
-
const coveredByPassingTests = passingTests.length > 0 &&
|
|
130
|
-
(criterionLower.includes("test") ||
|
|
131
|
-
criterionLower.includes("pass") ||
|
|
132
|
-
keywords.some((word) => passingTestText.includes(word)));
|
|
133
|
-
if (coveredByOutput || coveredByPassingTests) {
|
|
134
|
-
coveredCriteria.push(criterion);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
unmetCriteria.push(criterion);
|
|
138
|
-
findings.push({
|
|
139
|
-
dimension: "acceptance_coverage",
|
|
140
|
-
severity: "medium",
|
|
141
|
-
message: `Acceptance criterion not covered: '${criterion}'`,
|
|
142
|
-
criterion,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
const coverage = acceptanceCriteria.length > 0
|
|
147
|
-
? (coveredCriteria.length / acceptanceCriteria.length) * 100
|
|
148
|
-
: 100;
|
|
149
|
-
return { findings, coverage, unmetCriteria };
|
|
150
|
-
}
|
|
151
|
-
// --- Scoring -----------------------------------------------------------------
|
|
152
|
-
function calculateScore(weights, testAnalysis, acceptanceCoverage, criticalPolicies, securityFindings, regressionFindings) {
|
|
153
|
-
const breakdown = {};
|
|
154
|
-
// Correctness (30%) - based on missing outputs
|
|
155
|
-
const correctnessScore = weights.correctness;
|
|
156
|
-
breakdown.correctness = correctnessScore;
|
|
157
|
-
// Test completeness (20%)
|
|
158
|
-
let testScore = 0;
|
|
159
|
-
if (testAnalysis.totalCount > 0) {
|
|
160
|
-
testScore =
|
|
161
|
-
(testAnalysis.passedCount / testAnalysis.totalCount) * weights.test;
|
|
162
|
-
}
|
|
163
|
-
breakdown.tests = testScore;
|
|
164
|
-
// Acceptance coverage (20%)
|
|
165
|
-
const acceptanceScore = (acceptanceCoverage / 100) * weights.acceptance;
|
|
166
|
-
breakdown.acceptance = acceptanceScore;
|
|
167
|
-
// Code quality (15%) - assume passed if no major findings
|
|
168
|
-
const qualityScore = weights.quality;
|
|
169
|
-
breakdown.quality = qualityScore;
|
|
170
|
-
// Policy penalty (deducted)
|
|
171
|
-
let policyPenalty = 0;
|
|
172
|
-
for (const _ of criticalPolicies) {
|
|
173
|
-
policyPenalty += weights.policyPenalty;
|
|
174
|
-
}
|
|
175
|
-
breakdown.policyPenalty = -policyPenalty;
|
|
176
|
-
// Security penalty (deducted)
|
|
177
|
-
const securityPenalty = securityFindings.length * weights.securityPenalty;
|
|
178
|
-
breakdown.securityPenalty = -securityPenalty;
|
|
179
|
-
// Regression penalty (deducted)
|
|
180
|
-
const regressionPenalty = regressionFindings.length * weights.regressionPenalty;
|
|
181
|
-
breakdown.regressionPenalty = -regressionPenalty;
|
|
182
|
-
const totalScore = Math.max(0, Math.min(100, correctnessScore +
|
|
183
|
-
testScore +
|
|
184
|
-
acceptanceScore +
|
|
185
|
-
qualityScore -
|
|
186
|
-
policyPenalty -
|
|
187
|
-
securityPenalty -
|
|
188
|
-
regressionPenalty));
|
|
189
|
-
return { score: Math.round(totalScore), breakdown };
|
|
190
|
-
}
|
|
191
|
-
// --- Failure Classification ---------------------------------------------------
|
|
192
|
-
function classifyFailure(findings) {
|
|
193
|
-
const classes = [];
|
|
194
|
-
for (const finding of findings) {
|
|
195
|
-
if (finding.dimension === "test_completeness" &&
|
|
196
|
-
finding.severity === "critical") {
|
|
197
|
-
if (!classes.includes("test_failure"))
|
|
198
|
-
classes.push("test_failure");
|
|
199
|
-
}
|
|
200
|
-
if (finding.dimension === "correctness" && finding.severity === "high") {
|
|
201
|
-
if (!classes.includes("missing_output"))
|
|
202
|
-
classes.push("missing_output");
|
|
203
|
-
}
|
|
204
|
-
if (finding.dimension === "acceptance_coverage" &&
|
|
205
|
-
finding.severity === "medium") {
|
|
206
|
-
if (!classes.includes("acceptance_gap"))
|
|
207
|
-
classes.push("acceptance_gap");
|
|
208
|
-
}
|
|
209
|
-
if (finding.dimension === "policy_compliance") {
|
|
210
|
-
if (finding.severity === "critical") {
|
|
211
|
-
if (!classes.includes("policy_violation"))
|
|
212
|
-
classes.push("policy_violation");
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
if (finding.dimension === "security_risk") {
|
|
216
|
-
if (!classes.includes("security_issue"))
|
|
217
|
-
classes.push("security_issue");
|
|
218
|
-
}
|
|
219
|
-
if (finding.dimension === "regression_risk") {
|
|
220
|
-
if (!classes.includes("regression_risk"))
|
|
221
|
-
classes.push("regression_risk");
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if (classes.length === 0 && findings.length > 0) {
|
|
225
|
-
classes.push("insufficient_evidence");
|
|
226
|
-
}
|
|
227
|
-
return classes;
|
|
228
|
-
}
|
|
229
|
-
// --- Main Engine ---------------------------------------------------------------
|
|
230
|
-
export class EvaluationEngine {
|
|
231
|
-
weights;
|
|
232
|
-
state = "pending";
|
|
233
|
-
constructor(weights) {
|
|
234
|
-
this.weights = { ...DEFAULT_SCORING_WEIGHTS, ...weights };
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Evaluate a task against its acceptance criteria
|
|
238
|
-
*/
|
|
239
|
-
evaluate(request) {
|
|
240
|
-
this.state = "collecting_evidence";
|
|
241
|
-
const { jobId, task, outputs, testResults, policyFindings } = request;
|
|
242
|
-
const allFindings = [];
|
|
243
|
-
this.state = "evaluating";
|
|
244
|
-
// 1. Check for missing required outputs
|
|
245
|
-
const missingOutputs = findMissingOutputs(task.requiredArtifacts, outputs);
|
|
246
|
-
allFindings.push(...missingOutputs);
|
|
247
|
-
// 2. Analyze tests
|
|
248
|
-
const testAnalysis = analyzeTests(testResults);
|
|
249
|
-
allFindings.push(...testAnalysis.findings);
|
|
250
|
-
// 3. Analyze policies
|
|
251
|
-
const policyAnalysis = analyzePolicies(policyFindings);
|
|
252
|
-
allFindings.push(...policyAnalysis.findings);
|
|
253
|
-
// 4. Check acceptance criteria coverage
|
|
254
|
-
const acceptanceAnalysis = analyzeAcceptanceCoverage(task.acceptanceCriteria, outputs, testResults);
|
|
255
|
-
allFindings.push(...acceptanceAnalysis.findings);
|
|
256
|
-
// 5. Check for security issues
|
|
257
|
-
const securityFindings = allFindings.filter((f) => f.dimension === "security_risk");
|
|
258
|
-
// 6. Check for regression risks
|
|
259
|
-
const regressionFindings = allFindings.filter((f) => f.dimension === "regression_risk");
|
|
260
|
-
// 7. Calculate score
|
|
261
|
-
const { score, breakdown: _breakdown } = calculateScore(this.weights, testAnalysis, acceptanceAnalysis.coverage, policyAnalysis.criticalFindings, securityFindings, regressionFindings);
|
|
262
|
-
// 8. Collect evidence
|
|
263
|
-
const evidence = collectEvidence(outputs, testResults, policyFindings);
|
|
264
|
-
// 9. Determine status and recommended action
|
|
265
|
-
let status;
|
|
266
|
-
let recommendedAction;
|
|
267
|
-
// Critical policy violations force failure
|
|
268
|
-
if (policyAnalysis.criticalFindings.length > 0) {
|
|
269
|
-
status = "failed";
|
|
270
|
-
recommendedAction = "repair";
|
|
271
|
-
this.state = "failed";
|
|
272
|
-
}
|
|
273
|
-
// Test failures force failure
|
|
274
|
-
else if (testAnalysis.hasFailures) {
|
|
275
|
-
status = "failed";
|
|
276
|
-
recommendedAction = "repair";
|
|
277
|
-
this.state = "failed";
|
|
278
|
-
}
|
|
279
|
-
// Missing critical outputs
|
|
280
|
-
else if (missingOutputs.some((f) => f.severity === "critical")) {
|
|
281
|
-
status = "failed";
|
|
282
|
-
recommendedAction = "repair";
|
|
283
|
-
this.state = "failed";
|
|
284
|
-
}
|
|
285
|
-
// Low score with significant gaps
|
|
286
|
-
else if (score < 50) {
|
|
287
|
-
status = "failed";
|
|
288
|
-
recommendedAction = "repair";
|
|
289
|
-
this.state = "failed";
|
|
290
|
-
}
|
|
291
|
-
// Ambiguous criteria coverage
|
|
292
|
-
else if (acceptanceAnalysis.coverage < 80 &&
|
|
293
|
-
acceptanceAnalysis.unmetCriteria.length > 0) {
|
|
294
|
-
status = "needs_review";
|
|
295
|
-
recommendedAction = "human_review";
|
|
296
|
-
this.state = "needs_review";
|
|
297
|
-
}
|
|
298
|
-
// Good score
|
|
299
|
-
else {
|
|
300
|
-
status = "passed";
|
|
301
|
-
recommendedAction = "continue";
|
|
302
|
-
this.state = "passed";
|
|
303
|
-
}
|
|
304
|
-
// 10. Classify failure
|
|
305
|
-
const failureClasses = classifyFailure(allFindings);
|
|
306
|
-
// Add failure classification as a finding if failed
|
|
307
|
-
if (status === "failed" && failureClasses.length > 0) {
|
|
308
|
-
allFindings.push({
|
|
309
|
-
dimension: "correctness",
|
|
310
|
-
severity: "critical",
|
|
311
|
-
message: `Evaluation failed due to: ${failureClasses.join(", ")}`,
|
|
312
|
-
evidence: failureClasses.join(", "),
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
return {
|
|
316
|
-
status,
|
|
317
|
-
score,
|
|
318
|
-
findings: allFindings,
|
|
319
|
-
evidence,
|
|
320
|
-
recommendedAction,
|
|
321
|
-
evaluationId: randomUUID(),
|
|
322
|
-
jobId,
|
|
323
|
-
taskId: task.id,
|
|
324
|
-
evaluatedAt: new Date().toISOString(),
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Get current evaluation state
|
|
329
|
-
*/
|
|
330
|
-
getState() {
|
|
331
|
-
return this.state;
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Reset engine state
|
|
335
|
-
*/
|
|
336
|
-
reset() {
|
|
337
|
-
this.state = "pending";
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
// --- Factory -----------------------------------------------------------------
|
|
341
|
-
export function createEvaluationEngine(weights) {
|
|
342
|
-
return new EvaluationEngine(weights);
|
|
343
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Evaluation Engine Types (RFC-0057)
|
|
3
|
-
*
|
|
4
|
-
* Interfaces for task evaluation and acceptance criteria checking.
|
|
5
|
-
*/
|
|
6
|
-
export const DEFAULT_SCORING_WEIGHTS = {
|
|
7
|
-
correctness: 30,
|
|
8
|
-
test: 20,
|
|
9
|
-
acceptance: 20,
|
|
10
|
-
quality: 15,
|
|
11
|
-
policyPenalty: 20,
|
|
12
|
-
securityPenalty: 30,
|
|
13
|
-
regressionPenalty: 15,
|
|
14
|
-
};
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Experience Replay (RFC-0059)
|
|
3
|
-
*
|
|
4
|
-
* Reconstructs prior runtime execution from persisted events and checkpoints.
|
|
5
|
-
*/
|
|
6
|
-
// --- Event Timeline Reconstruction -------------------------------------------
|
|
7
|
-
function reconstructTimeline(events, startSequence = 0) {
|
|
8
|
-
return events.map((event, index) => ({
|
|
9
|
-
sequence: startSequence + index,
|
|
10
|
-
timestamp: event.ts,
|
|
11
|
-
type: event.type,
|
|
12
|
-
data: event.data || {},
|
|
13
|
-
}));
|
|
14
|
-
}
|
|
15
|
-
// --- State Reconstruction -----------------------------------------------------
|
|
16
|
-
function reconstructState(jobId, checkpoint, events, taskGraph) {
|
|
17
|
-
// Apply events to checkpoint to get final state
|
|
18
|
-
let currentTaskId = checkpoint?.currentTaskId;
|
|
19
|
-
let status = checkpoint?.status || "unknown";
|
|
20
|
-
// Process events to find latest state
|
|
21
|
-
for (const event of events) {
|
|
22
|
-
if (event.type === "task.started" && event.data?.taskId) {
|
|
23
|
-
currentTaskId = event.data.taskId;
|
|
24
|
-
}
|
|
25
|
-
if (event.type === "job.status") {
|
|
26
|
-
status = event.data?.status || status;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return {
|
|
30
|
-
jobId,
|
|
31
|
-
status,
|
|
32
|
-
currentTaskId,
|
|
33
|
-
checkpoint: checkpoint || {
|
|
34
|
-
version: 0,
|
|
35
|
-
jobId,
|
|
36
|
-
status: "unknown",
|
|
37
|
-
requirement: "",
|
|
38
|
-
createdAt: new Date().toISOString(),
|
|
39
|
-
updatedAt: new Date().toISOString(),
|
|
40
|
-
},
|
|
41
|
-
taskGraph: taskGraph || { nodes: [], edges: [] },
|
|
42
|
-
events,
|
|
43
|
-
timestamp: new Date().toISOString(),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
// --- Divergence Detection ----------------------------------------------------
|
|
47
|
-
// --- Source Validation --------------------------------------------------------
|
|
48
|
-
function validateSources(sources) {
|
|
49
|
-
const missing = [];
|
|
50
|
-
if (!sources.checkpoint && !sources.events) {
|
|
51
|
-
missing.push("checkpoint or events");
|
|
52
|
-
}
|
|
53
|
-
return { valid: missing.length === 0, missing };
|
|
54
|
-
}
|
|
55
|
-
// --- Mode Handlers ------------------------------------------------------------
|
|
56
|
-
async function handleInspect(sources, jobId, fromSequence, toSequence) {
|
|
57
|
-
const events = sources.events || [];
|
|
58
|
-
const filteredEvents = fromSequence !== undefined || toSequence !== undefined
|
|
59
|
-
? events.slice(fromSequence, toSequence)
|
|
60
|
-
: events;
|
|
61
|
-
const timeline = reconstructTimeline(filteredEvents, fromSequence ?? 0);
|
|
62
|
-
const state = reconstructState(jobId, sources.checkpoint, filteredEvents, sources.taskGraph);
|
|
63
|
-
return { state, timeline };
|
|
64
|
-
}
|
|
65
|
-
async function handleSimulate(sources, jobId, fromSequence, toSequence) {
|
|
66
|
-
const events = sources.events || [];
|
|
67
|
-
const filteredEvents = fromSequence !== undefined || toSequence !== undefined
|
|
68
|
-
? events.slice(fromSequence, toSequence)
|
|
69
|
-
: events;
|
|
70
|
-
const timeline = reconstructTimeline(filteredEvents, fromSequence ?? 0);
|
|
71
|
-
const state = reconstructState(jobId, sources.checkpoint, filteredEvents, sources.taskGraph);
|
|
72
|
-
// Check for divergent events
|
|
73
|
-
const divergences = [];
|
|
74
|
-
for (let i = 0; i < timeline.length; i++) {
|
|
75
|
-
const event = timeline[i];
|
|
76
|
-
if (event.type.includes("error") || event.type.includes("failure")) {
|
|
77
|
-
divergences.push({
|
|
78
|
-
sequence: event.sequence,
|
|
79
|
-
reason: "Provider output differs",
|
|
80
|
-
severity: "warning",
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return { state, timeline, divergences };
|
|
85
|
-
}
|
|
86
|
-
async function handleReexecute(sources, jobId, allowExternalCalls) {
|
|
87
|
-
// Reexecute requires explicit approval and valid sources
|
|
88
|
-
const validation = validateSources(sources);
|
|
89
|
-
if (!validation.valid) {
|
|
90
|
-
throw new Error(`Missing required sources: ${validation.missing.join(", ")}`);
|
|
91
|
-
}
|
|
92
|
-
if (!allowExternalCalls) {
|
|
93
|
-
throw new Error("Reexecution with external calls requires explicit approval");
|
|
94
|
-
}
|
|
95
|
-
// For reexecution, we simulate first then mark for actual reexecution
|
|
96
|
-
const result = await handleSimulate(sources, jobId);
|
|
97
|
-
// Add informational divergence for reexecution mode
|
|
98
|
-
result.divergences.push({
|
|
99
|
-
sequence: 0,
|
|
100
|
-
reason: "Runtime version differs",
|
|
101
|
-
severity: "info",
|
|
102
|
-
});
|
|
103
|
-
return result;
|
|
104
|
-
}
|
|
105
|
-
// --- Main Engine ---------------------------------------------------------------
|
|
106
|
-
export class ExperienceReplay {
|
|
107
|
-
eventListeners = [];
|
|
108
|
-
/**
|
|
109
|
-
* Replay a prior job execution
|
|
110
|
-
*/
|
|
111
|
-
async replay(request, sources) {
|
|
112
|
-
const { jobId, mode } = request;
|
|
113
|
-
// Emit start event
|
|
114
|
-
this.emit({ type: "replay.started", jobId, mode });
|
|
115
|
-
// Validate sources
|
|
116
|
-
const validation = validateSources(sources);
|
|
117
|
-
if (!validation.valid) {
|
|
118
|
-
return {
|
|
119
|
-
jobId,
|
|
120
|
-
reconstructedState: {
|
|
121
|
-
jobId,
|
|
122
|
-
status: "error",
|
|
123
|
-
checkpoint: sources.checkpoint || {},
|
|
124
|
-
taskGraph: sources.taskGraph || { nodes: [], edges: [] },
|
|
125
|
-
events: [],
|
|
126
|
-
timestamp: new Date().toISOString(),
|
|
127
|
-
},
|
|
128
|
-
timeline: [],
|
|
129
|
-
divergences: [
|
|
130
|
-
{
|
|
131
|
-
sequence: 0,
|
|
132
|
-
reason: `Missing sources: ${validation.missing.join(", ")}`,
|
|
133
|
-
severity: "error",
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
artifacts: [],
|
|
137
|
-
replayedAt: new Date().toISOString(),
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
let result;
|
|
142
|
-
switch (mode) {
|
|
143
|
-
case "inspect":
|
|
144
|
-
result = await handleInspect(sources, jobId, request.fromSequence, request.toSequence);
|
|
145
|
-
break;
|
|
146
|
-
case "simulate":
|
|
147
|
-
result = await handleSimulate(sources, jobId, request.fromSequence, request.toSequence);
|
|
148
|
-
break;
|
|
149
|
-
case "reexecute":
|
|
150
|
-
result = await handleReexecute(sources, jobId, request.allowExternalCalls);
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
// Emit divergence events
|
|
154
|
-
if (result.divergences) {
|
|
155
|
-
for (const div of result.divergences) {
|
|
156
|
-
this.emit({
|
|
157
|
-
type: "replay.divergence.detected",
|
|
158
|
-
jobId,
|
|
159
|
-
reason: div.reason,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
// Collect artifacts
|
|
164
|
-
const artifacts = [];
|
|
165
|
-
if (sources.prompts)
|
|
166
|
-
artifacts.push(...Object.keys(sources.prompts));
|
|
167
|
-
if (sources.outputs)
|
|
168
|
-
artifacts.push(...Object.keys(sources.outputs));
|
|
169
|
-
// Emit completion event
|
|
170
|
-
this.emit({ type: "replay.completed", jobId });
|
|
171
|
-
return {
|
|
172
|
-
jobId,
|
|
173
|
-
reconstructedState: result.state,
|
|
174
|
-
timeline: result.timeline,
|
|
175
|
-
divergences: result.divergences || [],
|
|
176
|
-
artifacts,
|
|
177
|
-
replayedAt: new Date().toISOString(),
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
182
|
-
return {
|
|
183
|
-
jobId,
|
|
184
|
-
reconstructedState: {
|
|
185
|
-
jobId,
|
|
186
|
-
status: "error",
|
|
187
|
-
checkpoint: sources.checkpoint || {},
|
|
188
|
-
taskGraph: sources.taskGraph || { nodes: [], edges: [] },
|
|
189
|
-
events: [],
|
|
190
|
-
timestamp: new Date().toISOString(),
|
|
191
|
-
},
|
|
192
|
-
timeline: [],
|
|
193
|
-
divergences: [
|
|
194
|
-
{
|
|
195
|
-
sequence: 0,
|
|
196
|
-
reason: errorMessage,
|
|
197
|
-
severity: "error",
|
|
198
|
-
},
|
|
199
|
-
],
|
|
200
|
-
artifacts: [],
|
|
201
|
-
replayedAt: new Date().toISOString(),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Subscribe to replay events
|
|
207
|
-
*/
|
|
208
|
-
onEvent(listener) {
|
|
209
|
-
this.eventListeners.push(listener);
|
|
210
|
-
return () => {
|
|
211
|
-
this.eventListeners = this.eventListeners.filter((l) => l !== listener);
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
emit(event) {
|
|
215
|
-
for (const listener of this.eventListeners) {
|
|
216
|
-
try {
|
|
217
|
-
listener(event);
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
// Best effort
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
// --- Factory -----------------------------------------------------------------
|
|
226
|
-
export function createExperienceReplay() {
|
|
227
|
-
return new ExperienceReplay();
|
|
228
|
-
}
|