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.
- package/.versionrc.js +32 -0
- package/cli.js +112 -0
- package/footer-status.js +155 -0
- package/harness/agent-handoff.js +123 -0
- package/harness/auto-compact.js +243 -0
- package/harness/auto-quota-resume.js +104 -0
- package/harness/blackboard.js +258 -0
- package/harness/context-compact-orchestrator.js +365 -0
- package/harness/context-window-manager.js +330 -0
- package/harness/continue-prompt.js +164 -0
- package/harness/e2e/minimax-quota-parser.js +52 -0
- package/harness/e2e/minimax-quota-scraper.js +475 -0
- package/harness/e2e/openai-quota-scraper.js +332 -0
- package/harness/e2e/playwright-runner.js +165 -0
- package/harness/e2e/quota-status.js +140 -0
- package/harness/e2e/test-engine.js +290 -0
- package/harness/forked-summarizer.js +212 -0
- package/harness/index.js +54 -0
- package/harness/job-state-machine.js +277 -0
- package/harness/loop-runtime.js +531 -0
- package/harness/master-planner.js +229 -0
- package/harness/notification-events.js +233 -0
- package/harness/output-limit-handler.js +233 -0
- package/harness/partial-recovery.js +413 -0
- package/harness/project-detector/detector.js +283 -0
- package/harness/repair-engine.js +256 -0
- package/harness/session-memory.js +293 -0
- package/harness/task-graph.js +87 -0
- package/index.js +1121 -0
- package/mirror.js +205 -0
- package/package.json +6 -4
- package/proactive-compact.js +42 -0
- package/renderer.js +134 -0
- package/status-parsers.js +63 -0
- package/tracker.js +49 -0
- package/windows.js +90 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop Runtime — RFC-0001
|
|
3
|
+
*
|
|
4
|
+
* Core execution loop for the harness runtime.
|
|
5
|
+
* Runs the repeated cycle:
|
|
6
|
+
* pick task -> assign model -> code -> run tests -> review diff
|
|
7
|
+
* -> if failed: repair
|
|
8
|
+
* -> if quota_limit: pause and resume later
|
|
9
|
+
* -> if context_full: compact + auto-resume
|
|
10
|
+
*
|
|
11
|
+
* Compact integration (RFC-0028):
|
|
12
|
+
* Uses CompactOrchestrator to proactively manage context window.
|
|
13
|
+
* On context_full: full compact + continue prompt + auto-retry
|
|
14
|
+
* On quota: pause job for later resume
|
|
15
|
+
*/
|
|
16
|
+
import { MirrorStore } from "../mirror.js";
|
|
17
|
+
import { JobStateMachine, } from "./job-state-machine.js";
|
|
18
|
+
import { CompactOrchestrator, } from "./context-compact-orchestrator.js";
|
|
19
|
+
import { createSessionMemoryManager, } from "./session-memory.js";
|
|
20
|
+
import { AutoCompactEngine } from "./auto-compact.js";
|
|
21
|
+
import { OutputLimitHandler } from "./output-limit-handler.js";
|
|
22
|
+
import { createForkedSummarizer, } from "./forked-summarizer.js";
|
|
23
|
+
// --- Constants ----------------------------------------------------------------
|
|
24
|
+
const DEFAULT_MAX_COMPACT_RETRIES = 3;
|
|
25
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 4096;
|
|
26
|
+
// --- Loop Runtime -----------------------------------------------------------
|
|
27
|
+
export class LoopRuntime {
|
|
28
|
+
config;
|
|
29
|
+
state;
|
|
30
|
+
jobState;
|
|
31
|
+
mirrorStore;
|
|
32
|
+
callbacks;
|
|
33
|
+
running = false;
|
|
34
|
+
paused = false;
|
|
35
|
+
// Compact-related state
|
|
36
|
+
maxCompactRetries;
|
|
37
|
+
maxOutputTokens;
|
|
38
|
+
compactOrchestrator;
|
|
39
|
+
forkedSummarizer;
|
|
40
|
+
sessionMemory;
|
|
41
|
+
autoCompactEngine;
|
|
42
|
+
totalCompactions = 0;
|
|
43
|
+
constructor(config, callbacks) {
|
|
44
|
+
this.callbacks = callbacks;
|
|
45
|
+
this.maxCompactRetries =
|
|
46
|
+
config.maxRepairAttempts ?? DEFAULT_MAX_COMPACT_RETRIES;
|
|
47
|
+
this.maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS;
|
|
48
|
+
this.config = {
|
|
49
|
+
jobId: config.jobId,
|
|
50
|
+
requirement: config.requirement,
|
|
51
|
+
providerPolicy: config.providerPolicy ?? {
|
|
52
|
+
plannerProvider: "openai",
|
|
53
|
+
codeProviders: ["openai"],
|
|
54
|
+
reviewProvider: "openai",
|
|
55
|
+
fallbackProviders: [],
|
|
56
|
+
},
|
|
57
|
+
maxIterations: config.maxIterations ?? 1000,
|
|
58
|
+
autoCheckpoint: config.autoCheckpoint ?? true,
|
|
59
|
+
checkpointInterval: config.checkpointInterval ?? 50,
|
|
60
|
+
pauseOnQuota: config.pauseOnQuota ?? true,
|
|
61
|
+
maxRepairAttempts: config.maxRepairAttempts ?? 3,
|
|
62
|
+
};
|
|
63
|
+
this.state = {
|
|
64
|
+
jobId: config.jobId,
|
|
65
|
+
iteration: 0,
|
|
66
|
+
status: "created",
|
|
67
|
+
currentTaskId: null,
|
|
68
|
+
lastCheckpoint: null,
|
|
69
|
+
};
|
|
70
|
+
this.jobState = new JobStateMachine({
|
|
71
|
+
checkpointManager: {
|
|
72
|
+
async save(ckpt) {
|
|
73
|
+
await callbacks.onCheckpoint?.(ckpt);
|
|
74
|
+
},
|
|
75
|
+
async load(_jobId) {
|
|
76
|
+
return null;
|
|
77
|
+
},
|
|
78
|
+
async appendEvent(_jobId, _event) {
|
|
79
|
+
// no-op for now
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
this.mirrorStore = new MirrorStore();
|
|
84
|
+
// Initialize session memory
|
|
85
|
+
this.sessionMemory = createSessionMemoryManager(config.jobId);
|
|
86
|
+
// Initialize auto-compact engine
|
|
87
|
+
this.autoCompactEngine = new AutoCompactEngine({
|
|
88
|
+
jobId: config.jobId,
|
|
89
|
+
requirement: config.requirement,
|
|
90
|
+
});
|
|
91
|
+
// Initialize compact orchestrator if agent callback provided
|
|
92
|
+
if (callbacks.onInvokeAgent) {
|
|
93
|
+
this.compactOrchestrator = new CompactOrchestrator({
|
|
94
|
+
jobId: config.jobId,
|
|
95
|
+
provider: callbacks.provider ?? "openai",
|
|
96
|
+
model: callbacks.summarizerModel ?? "gpt-4",
|
|
97
|
+
contextWindowSize: callbacks.contextWindowSize,
|
|
98
|
+
});
|
|
99
|
+
// Initialize forked summarizer if model provided
|
|
100
|
+
if (callbacks.summarizerModel && callbacks.onInvokeAgent) {
|
|
101
|
+
// Wrap onInvokeAgent to match ForkedSummarizer's InvokeOptions
|
|
102
|
+
const wrappedInvoke = async (opts) => {
|
|
103
|
+
return callbacks.onInvokeAgent(opts);
|
|
104
|
+
};
|
|
105
|
+
this.forkedSummarizer = createForkedSummarizer(callbacks.summarizerModel, wrappedInvoke);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// --- Public API ---------------------------------------------------------
|
|
110
|
+
async run() {
|
|
111
|
+
this.running = true;
|
|
112
|
+
// --- Auto-resume from quota pause ----------------------------------
|
|
113
|
+
const checkpoint = await this.callbacks.onGetCheckpoint?.();
|
|
114
|
+
if (checkpoint?.status === "paused_quota" && checkpoint.resumeAt) {
|
|
115
|
+
const resumeMs = new Date(checkpoint.resumeAt).getTime();
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
const waitMs = resumeMs - now;
|
|
118
|
+
if (waitMs > 0) {
|
|
119
|
+
const waitMin = Math.ceil(waitMs / 60_000);
|
|
120
|
+
const notify = this.callbacks.onNotify ?? (() => { });
|
|
121
|
+
await notify(`Quota reset in ${waitMin} min — auto-resuming at ${checkpoint.resumeAt}`, "info");
|
|
122
|
+
// Wait in 5-minute chunks so we don't block forever if quota resets early
|
|
123
|
+
while (this.running && Date.now() < resumeMs) {
|
|
124
|
+
await new Promise((r) => setTimeout(r, 5 * 60_000)); // 5 min
|
|
125
|
+
// Re-check mirror — quota may have reset early
|
|
126
|
+
const fresh = await this.callbacks.onCheckMirror?.("minimax");
|
|
127
|
+
if (fresh &&
|
|
128
|
+
fresh.h5_used_pct !== undefined &&
|
|
129
|
+
fresh.h5_used_pct < 100) {
|
|
130
|
+
break; // quota reset early, resume now
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Transition back to running
|
|
134
|
+
await this.jobState.transition("running");
|
|
135
|
+
this.callbacks.onQuotaEvent?.("resumed");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.state.status = "running";
|
|
139
|
+
try {
|
|
140
|
+
while (this.running) {
|
|
141
|
+
// Check for pause
|
|
142
|
+
if (this.paused) {
|
|
143
|
+
await this.saveCheckpoint();
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
this.state.iteration++;
|
|
147
|
+
// Check iteration limit
|
|
148
|
+
if (this.state.iteration > this.config.maxIterations) {
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
completed: false,
|
|
152
|
+
iterations: this.state.iteration,
|
|
153
|
+
error: "Max iterations exceeded",
|
|
154
|
+
totalCompactions: this.totalCompactions,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
this.callbacks.onIteration?.(this.state.iteration, this.state.status);
|
|
158
|
+
// Check job status
|
|
159
|
+
const statusSummary = await this.jobState.getStatusSummary();
|
|
160
|
+
if (statusSummary?.status === "paused_quota") {
|
|
161
|
+
await this.handleQuotaPause();
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
// Pick a task
|
|
165
|
+
const task = await this.callbacks.onPickTask?.();
|
|
166
|
+
if (!task) {
|
|
167
|
+
this.state.status = "archived";
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
this.state.currentTaskId = task.id;
|
|
171
|
+
await this.executeTaskWithCompact(task);
|
|
172
|
+
// Auto-checkpoint
|
|
173
|
+
if (this.config.autoCheckpoint &&
|
|
174
|
+
this.state.iteration % this.config.checkpointInterval === 0) {
|
|
175
|
+
await this.saveCheckpoint();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
success: this.state.status !== "archived",
|
|
180
|
+
completed: this.state.status === "archived",
|
|
181
|
+
iterations: this.state.iteration,
|
|
182
|
+
finalCheckpoint: this.state.lastCheckpoint ?? undefined,
|
|
183
|
+
totalCompactions: this.totalCompactions,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
this.running = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
pause() {
|
|
191
|
+
this.paused = true;
|
|
192
|
+
}
|
|
193
|
+
resume() {
|
|
194
|
+
this.paused = false;
|
|
195
|
+
this.state.status = "running";
|
|
196
|
+
}
|
|
197
|
+
stop() {
|
|
198
|
+
this.running = false;
|
|
199
|
+
}
|
|
200
|
+
getState() {
|
|
201
|
+
return { ...this.state };
|
|
202
|
+
}
|
|
203
|
+
/** Get compact orchestrator for external access */
|
|
204
|
+
getCompactOrchestrator() {
|
|
205
|
+
return this.compactOrchestrator;
|
|
206
|
+
}
|
|
207
|
+
/** Get session memory manager */
|
|
208
|
+
getSessionMemory() {
|
|
209
|
+
return this.sessionMemory;
|
|
210
|
+
}
|
|
211
|
+
// --- Task Execution with Compact ---------------------------------------
|
|
212
|
+
async executeTaskWithCompact(task) {
|
|
213
|
+
// 1. Pick model
|
|
214
|
+
const model = (await this.callbacks.onPickModel?.(task)) ?? "claude";
|
|
215
|
+
// 2. Build compactable messages
|
|
216
|
+
const compactMessages = [
|
|
217
|
+
{
|
|
218
|
+
role: "system",
|
|
219
|
+
content: this.buildSystemPrompt(task),
|
|
220
|
+
timestamp: Date.now(),
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
role: "user",
|
|
224
|
+
content: `Task: ${task.id}\nObjective: ${task.description ?? "See task graph."}`,
|
|
225
|
+
timestamp: Date.now(),
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
// 3. Inject session memory if available
|
|
229
|
+
const sessionMemoryContext = this.sessionMemory?.getMemoryForContext();
|
|
230
|
+
if (sessionMemoryContext) {
|
|
231
|
+
compactMessages.push({
|
|
232
|
+
role: "system",
|
|
233
|
+
content: sessionMemoryContext,
|
|
234
|
+
timestamp: Date.now(),
|
|
235
|
+
metadata: { sessionMemory: true },
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const outputLimitHandler = new OutputLimitHandler({
|
|
239
|
+
jobId: this.config.jobId,
|
|
240
|
+
taskId: task.id,
|
|
241
|
+
});
|
|
242
|
+
// 4. Execute with compact support
|
|
243
|
+
let compactRetries = 0;
|
|
244
|
+
while (compactRetries < this.maxCompactRetries) {
|
|
245
|
+
// Build invoke options
|
|
246
|
+
const invokeOptions = {
|
|
247
|
+
messages: compactMessages,
|
|
248
|
+
model,
|
|
249
|
+
maxOutputTokens: this.maxOutputTokens,
|
|
250
|
+
tools: task.tools,
|
|
251
|
+
systemPrompt: task.instructions,
|
|
252
|
+
};
|
|
253
|
+
// Use compact orchestrator if available
|
|
254
|
+
if (this.compactOrchestrator && this.callbacks.onInvokeAgent) {
|
|
255
|
+
const orchestratorCallbacks = this.buildCompactCallbacks(task, model);
|
|
256
|
+
const result = await this.compactOrchestrator.invokeWithCompact(invokeOptions, orchestratorCallbacks);
|
|
257
|
+
if (result.success) {
|
|
258
|
+
const continued = await this.handleAutoContinuation(compactMessages, result, task, outputLimitHandler);
|
|
259
|
+
if (continued) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
this.handleInvokeSuccess(compactMessages, result, task);
|
|
263
|
+
outputLimitHandler.reset();
|
|
264
|
+
await this.runTestsAndReview(task, compactMessages);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// Check for circuit breaker
|
|
268
|
+
if (this.compactOrchestrator.isCircuitBroken()) {
|
|
269
|
+
this.state.status = "blocked";
|
|
270
|
+
throw new Error(`Compact circuit breaker engaged after ${this.compactOrchestrator.getConsecutiveFailures()} failures.`);
|
|
271
|
+
}
|
|
272
|
+
// Compact happened — retry
|
|
273
|
+
if (result.compactResult) {
|
|
274
|
+
compactRetries++;
|
|
275
|
+
this.totalCompactions++;
|
|
276
|
+
this.onCompactionOccurred(result.compactResult);
|
|
277
|
+
// Append continue message
|
|
278
|
+
if (result.continueMessage) {
|
|
279
|
+
compactMessages.push({
|
|
280
|
+
role: "user",
|
|
281
|
+
content: result.continueMessage,
|
|
282
|
+
timestamp: Date.now(),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// Error without compact — surface it
|
|
288
|
+
throw new Error(result.error ?? "Unknown invoke error");
|
|
289
|
+
}
|
|
290
|
+
// Fallback: direct invoke without orchestrator
|
|
291
|
+
const result = await this.callbacks.onInvokeAgent?.(invokeOptions);
|
|
292
|
+
if (!result || !result.success) {
|
|
293
|
+
throw new Error(result?.error ?? "Invoke failed");
|
|
294
|
+
}
|
|
295
|
+
const continued = await this.handleAutoContinuation(compactMessages, result, task, outputLimitHandler);
|
|
296
|
+
if (continued) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
this.handleInvokeSuccess(compactMessages, result, task);
|
|
300
|
+
outputLimitHandler.reset();
|
|
301
|
+
await this.runTestsAndReview(task, compactMessages);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// Max compact retries exceeded
|
|
305
|
+
this.state.status = "blocked";
|
|
306
|
+
throw new Error(`Max compact retries (${this.maxCompactRetries}) exceeded`);
|
|
307
|
+
}
|
|
308
|
+
async handleAutoContinuation(messages, result, task, outputLimitHandler) {
|
|
309
|
+
const output = result.output ?? "";
|
|
310
|
+
const hitOutputLimit = outputLimitHandler.detectOutputLimit(result.error, {
|
|
311
|
+
finishReason: result.finishReason,
|
|
312
|
+
});
|
|
313
|
+
const hitCompactionBoundary = this.autoCompactEngine?.detectCompaction(output) ?? false;
|
|
314
|
+
if (!hitOutputLimit && !hitCompactionBoundary) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
if (!outputLimitHandler.shouldContinue()) {
|
|
318
|
+
throw new Error(`Output limit recovery exhausted after ${outputLimitHandler.getAttempts()} attempts for task ${task.id}.`);
|
|
319
|
+
}
|
|
320
|
+
this.handleInvokeSuccess(messages, result, task);
|
|
321
|
+
await outputLimitHandler.handleOutputLimit(output, result.finishReason ?? "length");
|
|
322
|
+
const continueMessage = hitCompactionBoundary
|
|
323
|
+
? (this.autoCompactEngine?.buildContinueMessage() ?? "continue")
|
|
324
|
+
: "Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if needed and continue the same task.";
|
|
325
|
+
messages.push({
|
|
326
|
+
role: "user",
|
|
327
|
+
content: continueMessage,
|
|
328
|
+
timestamp: Date.now(),
|
|
329
|
+
metadata: {
|
|
330
|
+
autoContinuation: true,
|
|
331
|
+
reason: hitCompactionBoundary ? "compaction" : "output_limit",
|
|
332
|
+
attempt: outputLimitHandler.getAttempts(),
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Build compact callbacks for the orchestrator
|
|
339
|
+
*/
|
|
340
|
+
buildCompactCallbacks(task, _model) {
|
|
341
|
+
return {
|
|
342
|
+
invokeAgent: async (opts) => {
|
|
343
|
+
return ((await this.callbacks.onInvokeAgent?.(opts)) ?? {
|
|
344
|
+
success: false,
|
|
345
|
+
error: "No agent",
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
onCheckpoint: async (result) => {
|
|
349
|
+
this.callbacks.onCompaction?.(result);
|
|
350
|
+
await this.saveCheckpoint();
|
|
351
|
+
},
|
|
352
|
+
onPreCompact: async (_reason) => {
|
|
353
|
+
// Save partial artifacts before compact
|
|
354
|
+
// Note: we don't have beforeTokens here, but we save the state
|
|
355
|
+
this.autoCompactEngine?.saveCompactionArtifact({
|
|
356
|
+
timestamp: new Date().toISOString(),
|
|
357
|
+
jobId: this.config.jobId,
|
|
358
|
+
taskId: task.id,
|
|
359
|
+
compactedFromTokens: 0,
|
|
360
|
+
reason: _reason,
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
onPostCompact: async (result) => {
|
|
364
|
+
// Extract facts from compacted messages for session memory
|
|
365
|
+
this.sessionMemory?.extractFacts([]);
|
|
366
|
+
this.callbacks.onCompaction?.(result);
|
|
367
|
+
},
|
|
368
|
+
onQuotaEvent: this.callbacks.onQuotaEvent,
|
|
369
|
+
summarizeViaForkedAgent: async (messages, reason) => {
|
|
370
|
+
if (this.forkedSummarizer) {
|
|
371
|
+
return this.forkedSummarizer.summarize(messages, {
|
|
372
|
+
focusOn: "work_done",
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
// Fallback to heuristic
|
|
376
|
+
return {
|
|
377
|
+
summary: `[Compact: ${reason}] Conversation summarised.`,
|
|
378
|
+
droppedCount: messages.length,
|
|
379
|
+
};
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Handle successful invoke result
|
|
385
|
+
*/
|
|
386
|
+
handleInvokeSuccess(messages, result, _task) {
|
|
387
|
+
// Append assistant response to messages
|
|
388
|
+
messages.push({
|
|
389
|
+
role: "assistant",
|
|
390
|
+
content: result.output ?? "",
|
|
391
|
+
timestamp: Date.now(),
|
|
392
|
+
metadata: {
|
|
393
|
+
model: result.model,
|
|
394
|
+
usage: result.usage,
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
// Extract facts from output for session memory
|
|
398
|
+
if (result.output) {
|
|
399
|
+
this.sessionMemory?.extractFacts([
|
|
400
|
+
{
|
|
401
|
+
role: "assistant",
|
|
402
|
+
content: result.output,
|
|
403
|
+
timestamp: Date.now(),
|
|
404
|
+
},
|
|
405
|
+
]);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Called when compaction occurs
|
|
410
|
+
*/
|
|
411
|
+
onCompactionOccurred(result) {
|
|
412
|
+
// Update session memory with compacted content
|
|
413
|
+
this.sessionMemory?.extractFacts([]);
|
|
414
|
+
// Save compaction event
|
|
415
|
+
this.autoCompactEngine?.saveCompactionArtifact({
|
|
416
|
+
timestamp: new Date().toISOString(),
|
|
417
|
+
jobId: this.config.jobId,
|
|
418
|
+
taskId: this.state.currentTaskId ?? "unknown",
|
|
419
|
+
compactedFromTokens: result.beforeTokens,
|
|
420
|
+
reason: result.trigger,
|
|
421
|
+
continuePrompt: result.summary,
|
|
422
|
+
});
|
|
423
|
+
// Notify callbacks
|
|
424
|
+
this.callbacks.onCompaction?.(result);
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Run tests and review
|
|
428
|
+
*/
|
|
429
|
+
async runTestsAndReview(task, messages) {
|
|
430
|
+
// Run tests
|
|
431
|
+
const unitPassed = (await this.callbacks.onRunTests?.(task)) ?? true;
|
|
432
|
+
const e2ePassed = (await this.callbacks.onRunE2E?.(task)) ?? true;
|
|
433
|
+
// Update session memory with test results
|
|
434
|
+
this.sessionMemory?.addTestResult(task.id, unitPassed && e2ePassed);
|
|
435
|
+
// Review
|
|
436
|
+
const verdict = await this.callbacks.onReview?.(task, {
|
|
437
|
+
unit: unitPassed,
|
|
438
|
+
e2e: e2ePassed,
|
|
439
|
+
});
|
|
440
|
+
if (verdict === "escalate") {
|
|
441
|
+
this.state.status = "waiting_human";
|
|
442
|
+
await this.callbacks.onEscalate?.(task, "Human review required");
|
|
443
|
+
this.paused = true;
|
|
444
|
+
}
|
|
445
|
+
else if (verdict === "repair") {
|
|
446
|
+
// Append repair instruction
|
|
447
|
+
messages.push({
|
|
448
|
+
role: "user",
|
|
449
|
+
content: "Tests failed. Please fix the issues and ensure all tests pass.",
|
|
450
|
+
timestamp: Date.now(),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Build system prompt for task
|
|
456
|
+
*/
|
|
457
|
+
buildSystemPrompt(task) {
|
|
458
|
+
const parts = [
|
|
459
|
+
task.instructions ?? "Complete the assigned task.",
|
|
460
|
+
"",
|
|
461
|
+
"## Context Management",
|
|
462
|
+
"- Keep responses concise and focused",
|
|
463
|
+
"- If context becomes full, the system will compact automatically",
|
|
464
|
+
"- Do not repeat work already done",
|
|
465
|
+
];
|
|
466
|
+
if (task.acceptanceCriteria && task.acceptanceCriteria.length > 0) {
|
|
467
|
+
parts.push("", "## Acceptance Criteria");
|
|
468
|
+
for (const criteria of task.acceptanceCriteria) {
|
|
469
|
+
parts.push(`- ${criteria}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return parts.join("\n");
|
|
473
|
+
}
|
|
474
|
+
// --- Quota Handling ----------------------------------------------------
|
|
475
|
+
async handleQuotaPause() {
|
|
476
|
+
this.paused = true;
|
|
477
|
+
this.state.status = "paused_quota";
|
|
478
|
+
this.jobState.transition("paused_quota");
|
|
479
|
+
// Set resumeAt from the mirror's known reset epoch so the auto-resume
|
|
480
|
+
// timer knows when to wake this job up.
|
|
481
|
+
const mirror = this.mirrorStore.readProvider("minimax");
|
|
482
|
+
const epoch = mirror?.h5_resets_at_epoch;
|
|
483
|
+
if (epoch) {
|
|
484
|
+
const resumeAt = new Date(epoch).toISOString();
|
|
485
|
+
await this.jobState.setResumeTime(resumeAt);
|
|
486
|
+
// console.log(
|
|
487
|
+
// `[LoopRuntime] Paused for 5h quota. Auto-resume at ${resumeAt}`,
|
|
488
|
+
// );
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
// console.log(
|
|
492
|
+
// "[LoopRuntime] Paused for quota but no reset time known yet.",
|
|
493
|
+
// );
|
|
494
|
+
}
|
|
495
|
+
await this.callbacks.onQuotaEvent?.("paused");
|
|
496
|
+
await this.saveCheckpoint();
|
|
497
|
+
}
|
|
498
|
+
// --- Persistence --------------------------------------------------------
|
|
499
|
+
async saveCheckpoint() {
|
|
500
|
+
const now = new Date().toISOString();
|
|
501
|
+
const checkpoint = {
|
|
502
|
+
version: 1,
|
|
503
|
+
jobId: this.state.jobId,
|
|
504
|
+
requirement: this.config.requirement,
|
|
505
|
+
taskId: this.state.currentTaskId ?? undefined,
|
|
506
|
+
iteration: this.state.iteration,
|
|
507
|
+
status: this.state.status,
|
|
508
|
+
createdAt: now,
|
|
509
|
+
updatedAt: now,
|
|
510
|
+
};
|
|
511
|
+
this.state.lastCheckpoint = checkpoint;
|
|
512
|
+
await this.jobState.transition(this.state.status);
|
|
513
|
+
await this.callbacks.onCheckpoint?.(checkpoint);
|
|
514
|
+
}
|
|
515
|
+
// --- Resume from Checkpoint --------------------------------------------
|
|
516
|
+
async resumeFromCheckpoint(checkpoint) {
|
|
517
|
+
this.state.iteration = checkpoint.iteration ?? 0;
|
|
518
|
+
this.state.currentTaskId = checkpoint.taskId ?? null;
|
|
519
|
+
this.state.status = checkpoint.status ?? "running";
|
|
520
|
+
this.paused = false;
|
|
521
|
+
// Check for pending continue prompt
|
|
522
|
+
if (this.autoCompactEngine?.hasContinuePrompt()) {
|
|
523
|
+
const continuePrompt = this.autoCompactEngine.loadContinuePrompt();
|
|
524
|
+
if (continuePrompt) {
|
|
525
|
+
// Resume will inject the continue prompt
|
|
526
|
+
this.callbacks.onIteration?.(this.state.iteration, this.state.status);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return this.run();
|
|
530
|
+
}
|
|
531
|
+
}
|