pi-harness-runtime 0.10.13 → 0.10.14

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.
@@ -0,0 +1,290 @@
1
+ /**
2
+ * E2E Test Engine — RFC-0013
3
+ *
4
+ * Browser-based end-to-end testing before work is marked ready for client.
5
+ *
6
+ * Artifact Layout:
7
+ * harness/e2e/
8
+ * scenarios/
9
+ * test-data/
10
+ * reports/
11
+ * artifacts/screenshots/
12
+ * artifacts/traces/
13
+ * artifacts/videos/
14
+ */
15
+ import { writeJson, appendJsonl } from "../../cli.js";
16
+ import { join } from "node:path";
17
+ export class E2ETestEngine {
18
+ rootDir;
19
+ config;
20
+ runner = null;
21
+ constructor(rootDir, config) {
22
+ this.rootDir = rootDir;
23
+ this.config = {
24
+ headless: true,
25
+ screenshotOnFailure: true,
26
+ videoOnFailure: false,
27
+ traceOnFailure: false,
28
+ timeout: 30000,
29
+ ...config,
30
+ };
31
+ }
32
+ /**
33
+ * Set the Playwright runner
34
+ */
35
+ setRunner(runner) {
36
+ this.runner = runner;
37
+ }
38
+ /**
39
+ * Run a single E2E scenario
40
+ */
41
+ async runScenario(scenario, context) {
42
+ if (!this.runner) {
43
+ return this.createErrorResult(scenario.id, "No runner configured");
44
+ }
45
+ const startTime = Date.now();
46
+ let stepsExecuted = 0;
47
+ let stepsPassed = 0;
48
+ let stepsFailed = 0;
49
+ let screenshotPath;
50
+ let failedStep;
51
+ try {
52
+ for (let i = 0; i < scenario.steps.length; i++) {
53
+ const step = scenario.steps[i];
54
+ stepsExecuted++;
55
+ try {
56
+ const success = await this.executeStep(step, context);
57
+ if (success) {
58
+ stepsPassed++;
59
+ }
60
+ else {
61
+ stepsFailed++;
62
+ failedStep = i;
63
+ if (this.config.screenshotOnFailure) {
64
+ screenshotPath = await this.captureScreenshot(scenario.id, i);
65
+ }
66
+ break; // Stop on first failure
67
+ }
68
+ }
69
+ catch (error) {
70
+ stepsFailed++;
71
+ failedStep = i;
72
+ if (this.config.screenshotOnFailure) {
73
+ screenshotPath = await this.captureScreenshot(scenario.id, i);
74
+ }
75
+ break;
76
+ }
77
+ }
78
+ const duration = Date.now() - startTime;
79
+ return {
80
+ scenarioId: scenario.id,
81
+ status: stepsFailed > 0 ? "failed" : "passed",
82
+ duration,
83
+ stepsExecuted,
84
+ stepsPassed,
85
+ stepsFailed,
86
+ screenshotPath,
87
+ executedAt: new Date().toISOString(),
88
+ failedStep,
89
+ };
90
+ }
91
+ catch (error) {
92
+ return this.createErrorResult(scenario.id, String(error));
93
+ }
94
+ }
95
+ /**
96
+ * Run all scenarios for a job
97
+ */
98
+ async runAllScenarios(jobId, scenarios, context) {
99
+ const results = [];
100
+ let totalDuration = 0;
101
+ for (const scenario of scenarios) {
102
+ if (!scenario.required) {
103
+ // Skip non-required scenarios on failure of required ones
104
+ const result = await this.runScenario(scenario, context);
105
+ results.push(result);
106
+ totalDuration += result.duration;
107
+ }
108
+ else {
109
+ const result = await this.runScenario(scenario, context);
110
+ results.push(result);
111
+ totalDuration += result.duration;
112
+ // Stop on first required failure
113
+ if (result.status === "failed") {
114
+ // Run remaining non-required scenarios
115
+ for (const nextScenario of scenarios.slice(scenarios.indexOf(scenario) + 1)) {
116
+ if (!nextScenario.required) {
117
+ const nextResult = await this.runScenario(nextScenario, context);
118
+ results.push(nextResult);
119
+ totalDuration += nextResult.duration;
120
+ }
121
+ }
122
+ break;
123
+ }
124
+ }
125
+ }
126
+ const report = {
127
+ jobId,
128
+ scenarios,
129
+ results,
130
+ summary: {
131
+ total: results.length,
132
+ passed: results.filter((r) => r.status === "passed").length,
133
+ failed: results.filter((r) => r.status === "failed").length,
134
+ skipped: results.filter((r) => r.status === "skipped").length,
135
+ duration: totalDuration,
136
+ },
137
+ createdAt: new Date().toISOString(),
138
+ };
139
+ // Save report
140
+ this.saveReport(report);
141
+ return report;
142
+ }
143
+ /**
144
+ * Create a scenario from a natural language description
145
+ */
146
+ createScenario(id, name, description, required = true) {
147
+ return {
148
+ id,
149
+ name,
150
+ description,
151
+ steps: [],
152
+ required,
153
+ };
154
+ }
155
+ /**
156
+ * Add a step to a scenario
157
+ */
158
+ addStep(scenario, action, options) {
159
+ scenario.steps.push({
160
+ action,
161
+ selector: options?.selector,
162
+ value: options?.value,
163
+ timeout: options?.timeout,
164
+ assertCondition: options?.assertCondition,
165
+ });
166
+ }
167
+ /**
168
+ * Execute a single step
169
+ */
170
+ async executeStep(step, context) {
171
+ if (!this.runner)
172
+ return false;
173
+ const timeout = step.timeout ?? this.config.timeout ?? 30000;
174
+ switch (step.action) {
175
+ case "navigate":
176
+ await this.runner.navigate(this.resolveValue(step.value ?? "", context));
177
+ return true;
178
+ case "click":
179
+ await this.runner.wait(step.selector, timeout);
180
+ await this.runner.click(step.selector);
181
+ return true;
182
+ case "type":
183
+ await this.runner.wait(step.selector, timeout);
184
+ await this.runner.type(step.selector, this.resolveValue(step.value ?? "", context));
185
+ return true;
186
+ case "wait":
187
+ await this.runner.wait(step.selector, timeout);
188
+ return true;
189
+ case "screenshot": {
190
+ const path = this.resolveValue(step.value ?? "screenshot.png", context);
191
+ await this.runner.screenshot(path);
192
+ return true;
193
+ }
194
+ case "assert":
195
+ return await this.runner.assert(step.assertCondition ?? "true", `Assertion failed: ${step.assertCondition}`);
196
+ case "hover":
197
+ await this.runner.wait(step.selector, timeout);
198
+ // await this.runner.hover(step.selector!);
199
+ return true;
200
+ case "select":
201
+ await this.runner.wait(step.selector, timeout);
202
+ // await this.runner.select(step.selector!, step.value!);
203
+ return true;
204
+ case "upload":
205
+ await this.runner.wait(step.selector, timeout);
206
+ // await this.runner.upload(step.selector!, step.value!);
207
+ return true;
208
+ default:
209
+ console.warn(`Unknown step action: ${step.action}`);
210
+ return false;
211
+ }
212
+ }
213
+ /**
214
+ * Capture screenshot
215
+ */
216
+ async captureScreenshot(scenarioId, stepIndex) {
217
+ const path = join(this.rootDir, "harness", "e2e", "artifacts", "screenshots", `${scenarioId}-step-${stepIndex}.png`);
218
+ if (this.runner) {
219
+ await this.runner.screenshot(path);
220
+ }
221
+ return path;
222
+ }
223
+ /**
224
+ * Create error result
225
+ */
226
+ createErrorResult(scenarioId, error) {
227
+ return {
228
+ scenarioId,
229
+ status: "error",
230
+ duration: 0,
231
+ stepsExecuted: 0,
232
+ stepsPassed: 0,
233
+ stepsFailed: 0,
234
+ errorMessage: error,
235
+ executedAt: new Date().toISOString(),
236
+ };
237
+ }
238
+ /**
239
+ * Save report to file
240
+ */
241
+ saveReport(report) {
242
+ const reportPath = join(this.rootDir, "harness", "e2e", "reports", `${report.jobId}-${Date.now()}.json`);
243
+ writeJson(reportPath, report);
244
+ // Also append to a log
245
+ const logPath = join(this.rootDir, "harness", "e2e", "reports", `${report.jobId}.jsonl`);
246
+ appendJsonl(logPath, report);
247
+ }
248
+ /**
249
+ * Resolve variables in values
250
+ */
251
+ resolveValue(value, context) {
252
+ if (!context)
253
+ return value;
254
+ let result = value;
255
+ for (const [key, val] of Object.entries(context)) {
256
+ result = result.replace(new RegExp(`{{${key}}}`, "g"), String(val));
257
+ }
258
+ return result;
259
+ }
260
+ /**
261
+ * Load scenarios from directory
262
+ */
263
+ loadScenarios(_scenariosDir) {
264
+ // In practice, this would read from the scenarios directory
265
+ return [];
266
+ }
267
+ }
268
+ /**
269
+ * Default E2E steps for common workflows
270
+ */
271
+ export const CommonSteps = {
272
+ login: (username, password) => [
273
+ { action: "navigate", value: "/login" },
274
+ { action: "type", selector: "#username", value: username },
275
+ { action: "type", selector: "#password", value: password },
276
+ { action: "click", selector: 'button[type="submit"]' },
277
+ { action: "wait", selector: ".dashboard", timeout: 10000 },
278
+ ],
279
+ logout: () => [
280
+ { action: "click", selector: ".user-menu" },
281
+ { action: "click", selector: 'a[href="/logout"]' },
282
+ ],
283
+ fillForm: (fields) => {
284
+ const steps = [];
285
+ for (const [selector, value] of Object.entries(fields)) {
286
+ steps.push({ action: "type", selector, value });
287
+ }
288
+ return steps;
289
+ },
290
+ };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Forked Summarizer — RFC-0028
3
+ *
4
+ * Summarizes old messages via a separate LLM call (forked context).
5
+ * This prevents the summarization request from hitting the same
6
+ * context-too-long error that triggered compaction.
7
+ */
8
+ // --- Default System Prompt ---------------------------------------------------
9
+ const DEFAULT_SUMMARY_SYSTEM = `You are a conversation summarizer. Your task is to create concise, accurate summaries that preserve essential context for continuing a conversation.
10
+
11
+ Focus on:
12
+ - What was the goal/problem being addressed?
13
+ - What approaches were tried and what were the results?
14
+ - What decisions were made and why?
15
+ - What remains to be done?
16
+ - What files or code sections are relevant?
17
+
18
+ IMPORTANT: Do NOT include tool_result content verbatim. Instead, summarize the outcomes.`;
19
+ const WORK_DONE_FOCUS = `Focus specifically on:
20
+ 1. What work was accomplished?
21
+ 2. What files were created, modified, or deleted?
22
+ 3. What tests passed or failed?
23
+ 4. What errors or issues were encountered and resolved?
24
+ 5. What is the current state of the project?`;
25
+ const DECISIONS_FOCUS = `Focus specifically on:
26
+ 1. What architectural decisions were made?
27
+ 2. What approaches were chosen over alternatives?
28
+ 3. What was the rationale for each decision?
29
+ 4. What constraints or requirements influenced the decisions?
30
+ 5. Any lessons learned or trade-offs considered?`;
31
+ // --- Forked Summarizer ------------------------------------------------------
32
+ export class ForkedSummarizer {
33
+ config;
34
+ invokeAgent;
35
+ constructor(config, invokeAgent) {
36
+ this.config = {
37
+ provider: "openai",
38
+ focusOn: "all",
39
+ ...config,
40
+ };
41
+ this.invokeAgent = invokeAgent;
42
+ }
43
+ /**
44
+ * Summarize old messages, keeping recent ones as context
45
+ */
46
+ async summarize(messages, options) {
47
+ const keepRecentCount = options?.keepRecentCount ?? 5;
48
+ const focusOn = options?.focusOn ?? this.config.focusOn ?? "all";
49
+ // Split messages
50
+ const recentMessages = messages.slice(-keepRecentCount);
51
+ const oldMessages = messages.slice(0, -keepRecentCount);
52
+ if (oldMessages.length === 0) {
53
+ return { summary: "", droppedCount: 0 };
54
+ }
55
+ // Build summary prompt
56
+ const summaryPrompt = this.buildSummaryPrompt(oldMessages, { focusOn });
57
+ // Create summarization messages
58
+ const summarizationMessages = [
59
+ {
60
+ role: "system",
61
+ content: this.config.systemPrompt ?? DEFAULT_SUMMARY_SYSTEM,
62
+ },
63
+ {
64
+ role: "user",
65
+ content: summaryPrompt,
66
+ },
67
+ ];
68
+ // Call summarization model
69
+ try {
70
+ const result = await this.invokeAgent({
71
+ messages: summarizationMessages,
72
+ model: this.config.model,
73
+ maxOutputTokens: this.config.maxSummaryTokens,
74
+ });
75
+ if (!result.success || !result.output) {
76
+ throw new Error(`Summarization failed: ${result.error ?? "unknown"}`);
77
+ }
78
+ return {
79
+ summary: result.output.trim(),
80
+ droppedCount: oldMessages.length,
81
+ tokensUsed: result.usage?.totalTokens,
82
+ };
83
+ }
84
+ catch (error) {
85
+ // Return heuristic summary on failure
86
+ return {
87
+ summary: this.heuristicSummary(oldMessages, focusOn),
88
+ droppedCount: oldMessages.length,
89
+ };
90
+ }
91
+ }
92
+ /**
93
+ * Build the summary prompt for the LLM
94
+ */
95
+ buildSummaryPrompt(messages, options) {
96
+ const focusOn = options.focusOn ?? "all";
97
+ const formattedMessages = messages
98
+ .map((m) => `[${m.role}]\n${this.truncateContent(m.content, 2000)}`)
99
+ .join("\n\n---\n\n");
100
+ const focusInstructions = {
101
+ work_done: WORK_DONE_FOCUS,
102
+ decisions: DECISIONS_FOCUS,
103
+ all: "Provide a comprehensive summary covering all important aspects of the conversation.",
104
+ }[focusOn];
105
+ return `Summarize the following conversation concisely.
106
+
107
+ ${focusInstructions}
108
+
109
+ ## Conversation to Summarize
110
+ ${formattedMessages}
111
+
112
+ ## Output Format
113
+ Provide a summary in 500-1000 tokens that:
114
+ 1. Captures the essential context
115
+ 2. Preserves key decisions and their rationale
116
+ 3. Notes any ongoing work or next steps
117
+ 4. Lists any important files or code sections referenced
118
+
119
+ Do not include verbatim tool results — summarize their outcomes instead.`;
120
+ }
121
+ /**
122
+ * Truncate content to max length
123
+ */
124
+ truncateContent(content, maxLength) {
125
+ if (content.length <= maxLength)
126
+ return content;
127
+ return content.substring(0, maxLength) + "... [truncated]";
128
+ }
129
+ /**
130
+ * Heuristic summary when LLM summarization fails
131
+ */
132
+ heuristicSummary(messages, focusOn) {
133
+ const summaries = [];
134
+ const keyFiles = new Set();
135
+ const decisions = [];
136
+ // Extract patterns from last 20 messages
137
+ for (const msg of messages.slice(-20)) {
138
+ const content = msg.content;
139
+ // Look for file references
140
+ const filePatterns = [
141
+ /created (?:file|module):?\s*([^\n.]+)/gi,
142
+ /modified (?:file|module):?\s*([^\n.]+)/gi,
143
+ /wrote to ([\w./-]+)/gi,
144
+ /updated ([\w./-]+)/gi,
145
+ ];
146
+ for (const pattern of filePatterns) {
147
+ for (const match of content.matchAll(pattern)) {
148
+ if (match[1])
149
+ keyFiles.add(match[1].trim());
150
+ }
151
+ }
152
+ // Look for decisions
153
+ const decisionPatterns = [
154
+ /(?:decided|choosing|choice|chose):?\s*([^\n.]+)/gi,
155
+ /(?:approach|strategy):?\s*([^\n.]+)/gi,
156
+ ];
157
+ for (const pattern of decisionPatterns) {
158
+ for (const match of content.matchAll(pattern)) {
159
+ if (match[1])
160
+ decisions.push(match[1].trim());
161
+ }
162
+ }
163
+ // Extract key user messages
164
+ if (msg.role === "user" && content.length > 50) {
165
+ summaries.push(`- User: ${content.substring(0, 300)}`);
166
+ }
167
+ // Extract significant actions from assistant
168
+ if (msg.role === "assistant" && msg.metadata?.action) {
169
+ summaries.push(`- Action: ${msg.metadata.action}`);
170
+ }
171
+ }
172
+ // Build heuristic summary
173
+ const parts = [
174
+ `[Context compacted — ${messages.length} messages summarized by heuristic]`,
175
+ "",
176
+ ];
177
+ if (keyFiles.size > 0) {
178
+ parts.push("## Files Referenced");
179
+ for (const file of Array.from(keyFiles).slice(0, 10)) {
180
+ parts.push(`- ${file}`);
181
+ }
182
+ parts.push("");
183
+ }
184
+ if (decisions.length > 0) {
185
+ parts.push("## Key Points");
186
+ for (const decision of Array.from(new Set(decisions)).slice(0, 5)) {
187
+ parts.push(`- ${decision}`);
188
+ }
189
+ parts.push("");
190
+ }
191
+ if (summaries.length > 0) {
192
+ parts.push("## Summary");
193
+ for (const summary of summaries.slice(0, 10)) {
194
+ parts.push(summary);
195
+ }
196
+ parts.push("");
197
+ }
198
+ parts.push("Please continue from the recent messages provided.");
199
+ return parts.join("\n");
200
+ }
201
+ }
202
+ // --- Convenience Factory ----------------------------------------------------
203
+ /**
204
+ * Create a ForkedSummarizer with default config
205
+ */
206
+ export function createForkedSummarizer(model, invokeAgent, config) {
207
+ return new ForkedSummarizer({
208
+ model,
209
+ maxSummaryTokens: 4096,
210
+ ...config,
211
+ }, invokeAgent);
212
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Harness Runtime — Core Module Index
3
+ *
4
+ * Re-exports all harness components for easy importing.
5
+ */
6
+ // State Machine
7
+ export { JobStateMachine } from "./job-state-machine.js";
8
+ // Loop Runtime
9
+ export { LoopRuntime } from "./loop-runtime.js";
10
+ // Repair Engine
11
+ export { RepairEngine } from "./repair-engine.js";
12
+ // Auto Quota Resume (5h window auto-resume)
13
+ export { scheduleAutoResume, cancelAutoResume, getScheduledResume, } from "./auto-quota-resume.js";
14
+ // Master Planner
15
+ export { MasterPlanner } from "./master-planner.js";
16
+ // Context Window Manager (RFC-0010 — enhanced with thresholds)
17
+ export { ContextWindowManager, microcompactToolResults, parseTokenGapFromError, AUTOCOMPACT_BUFFER_TOKENS, WARNING_THRESHOLD_BUFFER_TOKENS, BLOCKING_THRESHOLD_BUFFER_TOKENS, MAX_CONSECUTIVE_COMPACT_FAILURES, } from "./context-window-manager.js";
18
+ // Forked Summarizer (RFC-0028 Phase 2 — LLM-based compaction)
19
+ export { ForkedSummarizer, createForkedSummarizer, } from "./forked-summarizer.js";
20
+ // Continue Prompt Generator (RFC-0029 Phase 5 — auto-resume)
21
+ export { ContinuePromptGenerator, continuePromptGenerator, } from "./continue-prompt.js";
22
+ // BlackBoard
23
+ export { createBlackboard, SharedBlackboard } from "./blackboard.js";
24
+ // Agent Handoff
25
+ export { AgentHandoffProtocol } from "./agent-handoff.js";
26
+ // Auto Compact (RFC-0019)
27
+ export { AutoCompactEngine } from "./auto-compact.js";
28
+ // Context Compact Orchestrator (RFC-0028)
29
+ export { CompactOrchestrator } from "./context-compact-orchestrator.js";
30
+ // Output Limit Handler (RFC-0020)
31
+ export { OutputLimitHandler } from "./output-limit-handler.js";
32
+ // Partial Recovery (RFC-0021)
33
+ export { PartialRecovery, createPartialRecovery } from "./partial-recovery.js";
34
+ // Session Memory (RFC-0030)
35
+ export { SessionMemoryManager, createSessionMemoryManager, } from "./session-memory.js";
36
+ // Notification Events (RFC-0022)
37
+ export { HarnessNotificationEvents, createNotificationConfigFromEnv, } from "./notification-events.js";
38
+ // E2E Testing
39
+ export { E2ETestEngine } from "./e2e/test-engine.js";
40
+ export { PlaywrightE2ERunner } from "./e2e/playwright-runner.js";
41
+ export { MiniMaxQuotaScraper, MiniMaxQuotaManager, } from "./e2e/minimax-quota-scraper.js";
42
+ export { QuotaStatusManager, formatQuotaStatus, createQuotaStatusManagerFromEnv, } from "./e2e/quota-status.js";
43
+ // Project Detector
44
+ export { ProjectDetector } from "./project-detector/detector.js";
45
+ // --- RFC-0056: Performance Optimizer ----------------------------------------
46
+ export { PerformanceOptimizer, createPerformanceOptimizer, } from "../packages/performance-optimizer/src/index.js";
47
+ // --- RFC-0057: Evaluation Engine -------------------------------------------
48
+ export { EvaluationEngine, createEvaluationEngine, } from "../packages/evaluation-engine/src/index.js";
49
+ // --- RFC-0058: Learning Engine ---------------------------------------------
50
+ export { LearningEngine, createLearningEngine, } from "../packages/learning-engine/src/index.js";
51
+ // --- RFC-0059: Experience Replay ------------------------------------------
52
+ export { ExperienceReplay, createExperienceReplay, } from "../packages/experience-replay/src/index.js";
53
+ // --- RFC-0060: Memory Engine -----------------------------------------------
54
+ export { MemoryEngine, createMemoryEngine, } from "../packages/memory-engine/src/index.js";