loopforge 2.0.0 → 2.0.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/dist/backends/interface.d.ts +4 -3
- package/dist/backends/interface.d.ts.map +1 -1
- package/dist/backends/interface.js +4 -1
- package/dist/backends/interface.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/engine.d.ts +2 -24
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +72 -238
- package/dist/engine.js.map +1 -1
- package/dist/evidence-provider.d.ts +38 -5
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +131 -38
- package/dist/evidence-provider.js.map +1 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/interop.d.ts +5 -0
- package/dist/interop.d.ts.map +1 -1
- package/dist/interop.js +5 -0
- package/dist/interop.js.map +1 -1
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +4 -2
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +82 -0
- package/dist/loop-extras-parser.d.ts.map +1 -0
- package/dist/loop-extras-parser.js +217 -0
- package/dist/loop-extras-parser.js.map +1 -0
- package/dist/loop-store.d.ts +26 -4
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +96 -7
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/session.d.ts +27 -2
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +166 -117
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +1 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy.d.ts +0 -8
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +1 -9
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +25 -4
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/protocol.d.ts +2 -74
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +0 -26
- package/dist/protocol.js.map +1 -1
- package/dist/round-coordinator.d.ts +4 -4
- package/dist/round-coordinator.js +4 -4
- package/dist/self-eval.d.ts +26 -0
- package/dist/self-eval.d.ts.map +1 -0
- package/dist/self-eval.js +154 -0
- package/dist/self-eval.js.map +1 -0
- package/dist/storage.d.ts +3 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +57 -65
- package/dist/storage.js.map +1 -1
- package/dist/verification-gate.d.ts +16 -18
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +149 -94
- package/dist/verification-gate.js.map +1 -1
- package/loop_policy.json +1 -9
- package/package.json +1 -1
- package/dist/runtime.d.ts +0 -94
- package/dist/runtime.d.ts.map +0 -1
- package/dist/runtime.js +0 -718
- package/dist/runtime.js.map +0 -1
package/dist/runtime.js
DELETED
|
@@ -1,718 +0,0 @@
|
|
|
1
|
-
/** LoopForge — Loop Runtime (v1.2).
|
|
2
|
-
*
|
|
3
|
-
* Event-driven loop driver that wraps the compilation engine with
|
|
4
|
-
* heartbeat monitoring, timeout/stall detection, and graceful shutdown.
|
|
5
|
-
*
|
|
6
|
-
* Primary API:
|
|
7
|
-
* import { run } from 'loopforge';
|
|
8
|
-
* const result = await run({ task: '...', execute: myAgent });
|
|
9
|
-
*
|
|
10
|
-
* Advanced API:
|
|
11
|
-
* const rt = new LoopRuntime(config);
|
|
12
|
-
* rt.on('round:complete', (info) => console.log(info));
|
|
13
|
-
* const result = await rt.start();
|
|
14
|
-
*/
|
|
15
|
-
import { EventEmitter } from "node:events";
|
|
16
|
-
import { extractSelfEvaluation, heuristicSelfEvaluation, createEngine, } from "./engine.js";
|
|
17
|
-
import { Mode, RuntimeStatus, makeLoopCompileRequest, makeLoopRoundResult, makeSelfEvaluation, makeTaskId, } from "./protocol.js";
|
|
18
|
-
import { getPolicy } from "./policy.js";
|
|
19
|
-
import { prepareRoundTransaction, } from "./round-transaction.js";
|
|
20
|
-
import { RoundDriver } from "./round-driver.js";
|
|
21
|
-
import { logEvent } from "./observability.js";
|
|
22
|
-
import { policyMetrics } from "./policy-metrics.js";
|
|
23
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
-
// Helpers
|
|
25
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
26
|
-
function resolveConfig(raw) {
|
|
27
|
-
const policy = getPolicy();
|
|
28
|
-
const rtPolicy = policy.runtime;
|
|
29
|
-
const config = {
|
|
30
|
-
task: raw.task,
|
|
31
|
-
execute: raw.execute,
|
|
32
|
-
loopId: raw.loopId || makeTaskId(raw.task),
|
|
33
|
-
goalId: raw.goalId || "",
|
|
34
|
-
maxRounds: raw.maxRounds ?? rtPolicy.max_rounds,
|
|
35
|
-
roundTimeoutMs: raw.roundTimeoutMs ?? rtPolicy.round_timeout_ms,
|
|
36
|
-
heartbeatIntervalMs: raw.heartbeatIntervalMs ?? rtPolicy.heartbeat_interval_ms,
|
|
37
|
-
stallGraceMs: raw.stallGraceMs ?? rtPolicy.stall_grace_ms,
|
|
38
|
-
maxConsecutiveErrors: raw.maxConsecutiveErrors ?? rtPolicy.max_consecutive_errors,
|
|
39
|
-
interactive: raw.interactive ?? false,
|
|
40
|
-
healthCheckInterval: raw.healthCheckInterval ?? policy.summary.health_check_interval,
|
|
41
|
-
planSource: raw.planSource ?? undefined,
|
|
42
|
-
constraintsFromPlan: raw.constraintsFromPlan ?? [],
|
|
43
|
-
domain: raw.domain ?? "",
|
|
44
|
-
onRoundStart: raw.onRoundStart,
|
|
45
|
-
onRoundComplete: raw.onRoundComplete,
|
|
46
|
-
onHeartbeat: raw.onHeartbeat,
|
|
47
|
-
onTimeout: raw.onTimeout,
|
|
48
|
-
onHealthWarning: raw.onHealthWarning,
|
|
49
|
-
contextProvider: raw.contextProvider,
|
|
50
|
-
terminalSinks: raw.terminalSinks ?? [],
|
|
51
|
-
};
|
|
52
|
-
// Auto-detect claude-mem if no explicit provider was given
|
|
53
|
-
return config;
|
|
54
|
-
}
|
|
55
|
-
class RoundStalledError extends Error {
|
|
56
|
-
constructor() {
|
|
57
|
-
super("Round exceeded timeout and stall grace period");
|
|
58
|
-
this.name = "RoundStalledError";
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
62
|
-
// LoopRuntime
|
|
63
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
64
|
-
export class LoopRuntime extends EventEmitter {
|
|
65
|
-
config;
|
|
66
|
-
engine;
|
|
67
|
-
roundDriver;
|
|
68
|
-
_status = RuntimeStatus.IDLE;
|
|
69
|
-
currentRound = 1;
|
|
70
|
-
successTrajectory = [];
|
|
71
|
-
heartbeatTimer = null;
|
|
72
|
-
sigintHandler = null;
|
|
73
|
-
sigtermHandler = null;
|
|
74
|
-
// Verification gate state (v1.6 unified — was MCP-only, now also in runtime)
|
|
75
|
-
lastSelfEval = null;
|
|
76
|
-
pendingVerificationFlags = [];
|
|
77
|
-
// Enforcement gate state (v1.13)
|
|
78
|
-
consecutiveRejections = 0;
|
|
79
|
-
/** Which enforcement check triggered the last rejection.
|
|
80
|
-
* Only same-check rejections accumulate; a different check resets the counter. */
|
|
81
|
-
lastRejectionCheck = "";
|
|
82
|
-
pendingRejectionNotice = "";
|
|
83
|
-
roundSnapshot = null;
|
|
84
|
-
// Pause/resume state (v1.18)
|
|
85
|
-
pauseTimestamp = 0;
|
|
86
|
-
pauseRequested = false;
|
|
87
|
-
driverPromise = null;
|
|
88
|
-
// Explicit Agent-supplied context for the next compilation.
|
|
89
|
-
pendingExternalContext = "";
|
|
90
|
-
// Shared mutable state between the main loop and the heartbeat tick
|
|
91
|
-
roundStartTime = 0;
|
|
92
|
-
lastProgressTime = 0;
|
|
93
|
-
activeCtx = null;
|
|
94
|
-
activeAbortController = null;
|
|
95
|
-
timedOut = false;
|
|
96
|
-
constructor(rawConfig) {
|
|
97
|
-
super();
|
|
98
|
-
this.config = resolveConfig(rawConfig);
|
|
99
|
-
this.engine = createEngine();
|
|
100
|
-
this.roundDriver = new RoundDriver(this.engine);
|
|
101
|
-
}
|
|
102
|
-
get status() {
|
|
103
|
-
return this._status;
|
|
104
|
-
}
|
|
105
|
-
getCurrentRound() {
|
|
106
|
-
return this.currentRound;
|
|
107
|
-
}
|
|
108
|
-
getSuccessTrajectory() {
|
|
109
|
-
return [...this.successTrajectory];
|
|
110
|
-
}
|
|
111
|
-
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
112
|
-
/** Start the loop. Returns when the loop terminates (task complete,
|
|
113
|
-
* circuit breaker, max rounds, stalled, paused, or manual stop).
|
|
114
|
-
* Can only be called from IDLE state. */
|
|
115
|
-
async start() {
|
|
116
|
-
if (this._status !== RuntimeStatus.IDLE) {
|
|
117
|
-
throw new Error(`Cannot start: runtime is ${this._status}`);
|
|
118
|
-
}
|
|
119
|
-
this._status = RuntimeStatus.RUNNING;
|
|
120
|
-
this.registerSignalHandlers();
|
|
121
|
-
this.startHeartbeat();
|
|
122
|
-
this.emit("start");
|
|
123
|
-
return this.launchDriver(false);
|
|
124
|
-
}
|
|
125
|
-
/** Start the one allowed loop driver and clear its ownership on exit. */
|
|
126
|
-
launchDriver(isResume) {
|
|
127
|
-
if (this.driverPromise)
|
|
128
|
-
return this.driverPromise;
|
|
129
|
-
const driver = this._continue(isResume);
|
|
130
|
-
this.driverPromise = driver;
|
|
131
|
-
void driver.then(() => {
|
|
132
|
-
if (this.driverPromise === driver)
|
|
133
|
-
this.driverPromise = null;
|
|
134
|
-
}, () => {
|
|
135
|
-
if (this.driverPromise === driver)
|
|
136
|
-
this.driverPromise = null;
|
|
137
|
-
});
|
|
138
|
-
return driver;
|
|
139
|
-
}
|
|
140
|
-
/** Internal loop driver. Called by both start() (isResume=false) and
|
|
141
|
-
* resume() (isResume=true). When resuming, skips the currentRound=1
|
|
142
|
-
* reset — the loop picks up exactly where it left off. */
|
|
143
|
-
async _continue(isResume) {
|
|
144
|
-
let stopReason = "max_rounds";
|
|
145
|
-
let previousAgentOutput = null;
|
|
146
|
-
let consecutiveErrors = 0;
|
|
147
|
-
try {
|
|
148
|
-
// When starting fresh (not resuming), reset to round 1
|
|
149
|
-
if (!isResume) {
|
|
150
|
-
this.currentRound = 1;
|
|
151
|
-
}
|
|
152
|
-
for (; this.currentRound <= this.config.maxRounds; this.currentRound++) {
|
|
153
|
-
if (this.pauseRequested) {
|
|
154
|
-
this.pauseRequested = false;
|
|
155
|
-
this._status = RuntimeStatus.PAUSED;
|
|
156
|
-
stopReason = "paused";
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
if (this._status !== RuntimeStatus.RUNNING) {
|
|
160
|
-
if (this._status === RuntimeStatus.PAUSED) {
|
|
161
|
-
stopReason = "paused";
|
|
162
|
-
}
|
|
163
|
-
else if (this._status === RuntimeStatus.STALLED) {
|
|
164
|
-
stopReason = "stalled";
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
stopReason = "cancelled";
|
|
168
|
-
}
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
// ── Explicit external context ─────────────────────────────────
|
|
172
|
-
this.pendingExternalContext = "";
|
|
173
|
-
if (this.config.contextProvider) {
|
|
174
|
-
try {
|
|
175
|
-
this.pendingExternalContext = (await this.config.contextProvider({
|
|
176
|
-
loopId: this.config.loopId,
|
|
177
|
-
round: this.currentRound,
|
|
178
|
-
task: this.config.task,
|
|
179
|
-
domain: this.config.domain,
|
|
180
|
-
lastEvaluation: this.lastSelfEval ?? undefined,
|
|
181
|
-
})).trim();
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
// Provider failures are isolated from loop execution.
|
|
185
|
-
this.pendingExternalContext = "";
|
|
186
|
-
logEvent("context_provider_error", {
|
|
187
|
-
loopId: this.config.loopId,
|
|
188
|
-
round: this.currentRound,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
// ── Compile ──────────────────────────────────────────────────
|
|
193
|
-
let prompt;
|
|
194
|
-
let level;
|
|
195
|
-
if (this.pendingRejectionNotice) {
|
|
196
|
-
if (!this.roundSnapshot || this.roundSnapshot.phase !== "rejected") {
|
|
197
|
-
stopReason = "stalled";
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
const retryRequest = this.buildEngineCompileRequest(null);
|
|
201
|
-
const prepared = await this.roundDriver.prepareRetry(retryRequest, this.roundSnapshot, this.pendingRejectionNotice, this.consecutiveRejections);
|
|
202
|
-
if (!prepared) {
|
|
203
|
-
stopReason = "stalled";
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
prompt = prepared.prompt;
|
|
207
|
-
level = prepared.level;
|
|
208
|
-
this.roundSnapshot = prepared.snapshot;
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
const compileRequest = this.buildEngineCompileRequest(previousAgentOutput);
|
|
212
|
-
const prepared = await this.roundDriver.prepare(compileRequest, this.config.loopId, this.currentRound);
|
|
213
|
-
if (!prepared) {
|
|
214
|
-
stopReason = "stalled";
|
|
215
|
-
break;
|
|
216
|
-
}
|
|
217
|
-
prompt = prepared.prompt;
|
|
218
|
-
level = prepared.level;
|
|
219
|
-
this.roundSnapshot = prepared.snapshot;
|
|
220
|
-
}
|
|
221
|
-
// ── Emit round:start ──────────────────────────────────────────
|
|
222
|
-
if (!this.roundSnapshot) {
|
|
223
|
-
this.roundSnapshot = prepareRoundTransaction(this.config.loopId, this.currentRound, []);
|
|
224
|
-
}
|
|
225
|
-
const roundId = this.roundSnapshot.roundId;
|
|
226
|
-
policyMetrics.recordStrategy(this.config.loopId, level);
|
|
227
|
-
const startInfo = {
|
|
228
|
-
round: this.currentRound,
|
|
229
|
-
roundId,
|
|
230
|
-
level,
|
|
231
|
-
prompt,
|
|
232
|
-
};
|
|
233
|
-
this.emit("round:start", startInfo);
|
|
234
|
-
this.config.onRoundStart?.(startInfo);
|
|
235
|
-
// ── Execute ───────────────────────────────────────────────────
|
|
236
|
-
this.roundStartTime = Date.now();
|
|
237
|
-
this.lastProgressTime = this.roundStartTime;
|
|
238
|
-
this.timedOut = false;
|
|
239
|
-
const abortController = new AbortController();
|
|
240
|
-
const ctx = {
|
|
241
|
-
round: this.currentRound,
|
|
242
|
-
roundId,
|
|
243
|
-
signal: abortController.signal,
|
|
244
|
-
reportProgress: (message) => {
|
|
245
|
-
this.lastProgressTime = Date.now();
|
|
246
|
-
void message; // consumed by heartbeat via lastProgressTime
|
|
247
|
-
},
|
|
248
|
-
};
|
|
249
|
-
this.activeCtx = ctx;
|
|
250
|
-
this.activeAbortController = abortController;
|
|
251
|
-
let agentOutput;
|
|
252
|
-
try {
|
|
253
|
-
agentOutput = await this.executeWithDeadline(prompt, ctx);
|
|
254
|
-
consecutiveErrors = 0;
|
|
255
|
-
}
|
|
256
|
-
catch (err) {
|
|
257
|
-
// Calls from timers/signal handlers can mutate status while this await
|
|
258
|
-
// is pending; TypeScript's control-flow narrowing cannot observe that.
|
|
259
|
-
const statusAfterExecution = this._status;
|
|
260
|
-
// A cooperative executor may reject as soon as its AbortSignal fires,
|
|
261
|
-
// winning the race against RoundStalledError. The runtime's state is
|
|
262
|
-
// authoritative in that case: timeout/stall is not an executor fault.
|
|
263
|
-
if (err instanceof RoundStalledError ||
|
|
264
|
-
this.timedOut ||
|
|
265
|
-
statusAfterExecution === RuntimeStatus.STALLED) {
|
|
266
|
-
this.activeCtx = null;
|
|
267
|
-
this.activeAbortController = null;
|
|
268
|
-
stopReason = "stalled";
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
if (statusAfterExecution === RuntimeStatus.STOPPED) {
|
|
272
|
-
this.activeCtx = null;
|
|
273
|
-
this.activeAbortController = null;
|
|
274
|
-
stopReason = "cancelled";
|
|
275
|
-
break;
|
|
276
|
-
}
|
|
277
|
-
consecutiveErrors++;
|
|
278
|
-
const dur = Date.now() - this.roundStartTime;
|
|
279
|
-
this.activeCtx = null;
|
|
280
|
-
this.activeAbortController = null;
|
|
281
|
-
// Emit round:complete with error info
|
|
282
|
-
const errInfo = {
|
|
283
|
-
round: this.currentRound,
|
|
284
|
-
roundSuccess: false,
|
|
285
|
-
selfEval: makeSelfEvaluation({
|
|
286
|
-
success: false,
|
|
287
|
-
output_summary: `execute threw: ${String(err).slice(0, 200)}`,
|
|
288
|
-
constraint_violations: ["execute() threw an exception"],
|
|
289
|
-
should_continue: true,
|
|
290
|
-
}),
|
|
291
|
-
durationMs: dur,
|
|
292
|
-
};
|
|
293
|
-
this.emit("round:complete", errInfo);
|
|
294
|
-
this.config.onRoundComplete?.(errInfo);
|
|
295
|
-
this.successTrajectory.push(false);
|
|
296
|
-
if (consecutiveErrors >= this.config.maxConsecutiveErrors) {
|
|
297
|
-
stopReason = "executor_failure";
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
// Build a synthetic self-eval so the next round's compilation
|
|
301
|
-
// sees the failure context (forces L1/L2). A null previousAgentOutput
|
|
302
|
-
// would hide the crash from the compiler entirely.
|
|
303
|
-
previousAgentOutput = `---loopforge-eval\n${JSON.stringify(errInfo.selfEval)}\n---end-loopforge-eval`;
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
const durationMs = Date.now() - this.roundStartTime;
|
|
307
|
-
this.activeCtx = null;
|
|
308
|
-
this.activeAbortController = null;
|
|
309
|
-
// ── Extract self-eval ─────────────────────────────────────────
|
|
310
|
-
let selfEval = extractSelfEvaluation(agentOutput);
|
|
311
|
-
let extractionSucceeded = selfEval !== null;
|
|
312
|
-
if (selfEval === null) {
|
|
313
|
-
selfEval = heuristicSelfEvaluation(agentOutput);
|
|
314
|
-
extractionSucceeded = false;
|
|
315
|
-
}
|
|
316
|
-
// ── Unified round transaction: evaluate → verify → commit/reject ──
|
|
317
|
-
this.pendingVerificationFlags = [];
|
|
318
|
-
let roundSuccess = selfEval?.success ?? false;
|
|
319
|
-
if (selfEval !== null) {
|
|
320
|
-
const completed = await this.roundDriver.complete({
|
|
321
|
-
snapshot: this.roundSnapshot,
|
|
322
|
-
loopId: this.config.loopId,
|
|
323
|
-
task: this.config.task,
|
|
324
|
-
maxRounds: this.config.maxRounds,
|
|
325
|
-
selfEval,
|
|
326
|
-
extractionSucceeded,
|
|
327
|
-
lastSelfEval: this.lastSelfEval ?? undefined,
|
|
328
|
-
consecutiveRejections: this.consecutiveRejections,
|
|
329
|
-
successTrajectory: this.successTrajectory,
|
|
330
|
-
});
|
|
331
|
-
const outcome = completed.outcome;
|
|
332
|
-
this.roundSnapshot = outcome.snapshot;
|
|
333
|
-
const pr = outcome.result;
|
|
334
|
-
policyMetrics.recordStrategyOutcome(this.config.loopId, level, pr, outcome.replayed);
|
|
335
|
-
this.pendingVerificationFlags = pr.verificationFlags;
|
|
336
|
-
roundSuccess = pr.roundSuccess;
|
|
337
|
-
// Track per-rule rejections: only same-check rejections
|
|
338
|
-
// accumulate. A different rejection reason resets the counter.
|
|
339
|
-
if (pr.action === "reject" && pr.rejectionCheck) {
|
|
340
|
-
this.consecutiveRejections =
|
|
341
|
-
pr.rejectionCheck === this.lastRejectionCheck
|
|
342
|
-
? pr.newConsecutiveRejections
|
|
343
|
-
: 1;
|
|
344
|
-
this.lastRejectionCheck = pr.rejectionCheck;
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
this.consecutiveRejections = pr.newConsecutiveRejections;
|
|
348
|
-
if (pr.action !== "reject")
|
|
349
|
-
this.lastRejectionCheck = "";
|
|
350
|
-
}
|
|
351
|
-
if (pr.newLastSelfEval)
|
|
352
|
-
this.lastSelfEval = pr.newLastSelfEval;
|
|
353
|
-
if (pr.shouldPushSuccessTrajectory)
|
|
354
|
-
this.successTrajectory.push(roundSuccess);
|
|
355
|
-
if (pr.action === "reject") {
|
|
356
|
-
this.pendingRejectionNotice = pr.rejectionPrompt ?? "";
|
|
357
|
-
this.currentRound--;
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
// The retry was accepted. Future rounds must return to normal compile.
|
|
361
|
-
this.pendingRejectionNotice = "";
|
|
362
|
-
if (pr.action === "terminate") {
|
|
363
|
-
stopReason = (pr.stopReason ?? "enforcement_terminated");
|
|
364
|
-
break;
|
|
365
|
-
}
|
|
366
|
-
if (pr.action === "stop") {
|
|
367
|
-
stopReason = (pr.stopReason ?? "stalled");
|
|
368
|
-
break;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
else {
|
|
372
|
-
// Null selfEval: extraction completely failed
|
|
373
|
-
stopReason = "stalled";
|
|
374
|
-
break;
|
|
375
|
-
}
|
|
376
|
-
// ── Emit round:complete ───────────────────────────────────────
|
|
377
|
-
const completeInfo = {
|
|
378
|
-
round: this.currentRound,
|
|
379
|
-
roundId,
|
|
380
|
-
roundSuccess,
|
|
381
|
-
selfEval,
|
|
382
|
-
durationMs,
|
|
383
|
-
};
|
|
384
|
-
this.emit("round:complete", completeInfo);
|
|
385
|
-
this.config.onRoundComplete?.(completeInfo);
|
|
386
|
-
logEvent("round_complete", {
|
|
387
|
-
loopId: this.config.loopId ?? "unknown",
|
|
388
|
-
round: this.currentRound,
|
|
389
|
-
success: roundSuccess,
|
|
390
|
-
durationMs,
|
|
391
|
-
});
|
|
392
|
-
// ── Post-round state ──────────────────────────────────────────
|
|
393
|
-
if (this.timedOut) {
|
|
394
|
-
// Next round will be forced L2 via the compile request
|
|
395
|
-
this.timedOut = false;
|
|
396
|
-
}
|
|
397
|
-
previousAgentOutput = agentOutput;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
finally {
|
|
401
|
-
// ── Cleanup — always runs even if the loop body throws ──────────
|
|
402
|
-
// Only fully stop if we're not paused (paused loops stay alive)
|
|
403
|
-
if (this._status !== RuntimeStatus.PAUSED) {
|
|
404
|
-
this.stopHeartbeat();
|
|
405
|
-
this.unregisterSignalHandlers();
|
|
406
|
-
if (this._status === RuntimeStatus.RUNNING) {
|
|
407
|
-
this._status = RuntimeStatus.STOPPED;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
const result = {
|
|
412
|
-
success: stopReason === "completed",
|
|
413
|
-
stopReason,
|
|
414
|
-
roundsCompleted: this.currentRound > this.config.maxRounds
|
|
415
|
-
? this.config.maxRounds
|
|
416
|
-
: this.currentRound,
|
|
417
|
-
successTrajectory: [...this.successTrajectory],
|
|
418
|
-
};
|
|
419
|
-
// ── v1.7: Memory Writeback ───────────────────────────────────────
|
|
420
|
-
// Skip writeback for paused loops — they may be resumed later
|
|
421
|
-
if (stopReason !== "paused" && this.config.terminalSinks.length > 0) {
|
|
422
|
-
const event = {
|
|
423
|
-
...result,
|
|
424
|
-
loopId: this.config.loopId,
|
|
425
|
-
task: this.config.task,
|
|
426
|
-
lastEvaluation: this.lastSelfEval ?? undefined,
|
|
427
|
-
};
|
|
428
|
-
await Promise.allSettled(this.config.terminalSinks.map((sink) => Promise.resolve(sink(event))));
|
|
429
|
-
}
|
|
430
|
-
this.emit("done", result);
|
|
431
|
-
return result;
|
|
432
|
-
}
|
|
433
|
-
/** Execute one round with a hard driver deadline. The AbortSignal lets a
|
|
434
|
-
* cooperative executor stop early; the race guarantees the runtime itself
|
|
435
|
-
* still resolves when an executor ignores cancellation. */
|
|
436
|
-
async executeWithDeadline(prompt, ctx) {
|
|
437
|
-
if (this.config.interactive) {
|
|
438
|
-
return this.config.execute(prompt, ctx);
|
|
439
|
-
}
|
|
440
|
-
const deadlineMs = this.config.roundTimeoutMs + this.config.stallGraceMs;
|
|
441
|
-
let timer = null;
|
|
442
|
-
const deadline = new Promise((_resolve, reject) => {
|
|
443
|
-
timer = setTimeout(() => {
|
|
444
|
-
const elapsed = Date.now() - this.roundStartTime;
|
|
445
|
-
this.triggerTimeout(elapsed);
|
|
446
|
-
this.triggerStall(elapsed);
|
|
447
|
-
reject(new RoundStalledError());
|
|
448
|
-
}, deadlineMs);
|
|
449
|
-
});
|
|
450
|
-
try {
|
|
451
|
-
return await Promise.race([this.config.execute(prompt, ctx), deadline]);
|
|
452
|
-
}
|
|
453
|
-
finally {
|
|
454
|
-
if (timer !== null)
|
|
455
|
-
clearTimeout(timer);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
triggerTimeout(elapsed) {
|
|
459
|
-
if (this.timedOut)
|
|
460
|
-
return;
|
|
461
|
-
this.timedOut = true;
|
|
462
|
-
this.activeAbortController?.abort();
|
|
463
|
-
const timeoutInfo = {
|
|
464
|
-
round: this.currentRound,
|
|
465
|
-
elapsedMs: elapsed,
|
|
466
|
-
};
|
|
467
|
-
this.emit("timeout", timeoutInfo);
|
|
468
|
-
this.config.onTimeout?.(timeoutInfo);
|
|
469
|
-
}
|
|
470
|
-
triggerStall(elapsed) {
|
|
471
|
-
if (this._status === RuntimeStatus.STALLED)
|
|
472
|
-
return;
|
|
473
|
-
this._status = RuntimeStatus.STALLED;
|
|
474
|
-
const stallMsg = `Round ${this.currentRound} stalled after ${elapsed}ms`;
|
|
475
|
-
this.emit("stalled", {
|
|
476
|
-
reason: "round_timeout",
|
|
477
|
-
lastRound: this.currentRound,
|
|
478
|
-
elapsedMs: elapsed,
|
|
479
|
-
message: stallMsg,
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
/** Stop the loop gracefully. Safe to call from any thread/timer. */
|
|
483
|
-
stop() {
|
|
484
|
-
if (this._status === RuntimeStatus.RUNNING ||
|
|
485
|
-
this._status === RuntimeStatus.PAUSED) {
|
|
486
|
-
const driverActive = this.driverPromise !== null;
|
|
487
|
-
this.pauseRequested = false;
|
|
488
|
-
this.activeAbortController?.abort();
|
|
489
|
-
this._status = RuntimeStatus.STOPPED;
|
|
490
|
-
this.emit("stop");
|
|
491
|
-
if (!driverActive) {
|
|
492
|
-
this.stopHeartbeat();
|
|
493
|
-
this.unregisterSignalHandlers();
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
/** Pause the loop gracefully. Completes the current round, then suspends.
|
|
498
|
-
* Safe to call from signal handlers or callbacks.
|
|
499
|
-
* Only has effect when the loop is RUNNING. */
|
|
500
|
-
pause() {
|
|
501
|
-
if (this._status === RuntimeStatus.RUNNING && !this.pauseRequested) {
|
|
502
|
-
this.pauseRequested = true;
|
|
503
|
-
this.pauseTimestamp = Date.now();
|
|
504
|
-
this.emit("pause");
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
/** Resume a paused loop. Re-enters the main loop from currentRound.
|
|
508
|
-
* The loop continues exactly where it was suspended. */
|
|
509
|
-
async resume() {
|
|
510
|
-
// A resume requested before the current round reaches its pause boundary
|
|
511
|
-
// cancels that pending pause and reuses the existing driver.
|
|
512
|
-
if (this._status === RuntimeStatus.RUNNING && this.pauseRequested) {
|
|
513
|
-
this.pauseRequested = false;
|
|
514
|
-
this.pauseTimestamp = 0;
|
|
515
|
-
this.emit("resume");
|
|
516
|
-
if (!this.driverPromise) {
|
|
517
|
-
throw new Error("Cannot resume: active runtime has no driver");
|
|
518
|
-
}
|
|
519
|
-
return this.driverPromise;
|
|
520
|
-
}
|
|
521
|
-
if (this._status !== RuntimeStatus.PAUSED) {
|
|
522
|
-
throw new Error(`Cannot resume: runtime is ${this._status}`);
|
|
523
|
-
}
|
|
524
|
-
this._status = RuntimeStatus.RUNNING;
|
|
525
|
-
this.pauseTimestamp = 0;
|
|
526
|
-
this.emit("resume");
|
|
527
|
-
return this.launchDriver(true);
|
|
528
|
-
}
|
|
529
|
-
// ── Internal: compile request builder ─────────────────────────────────
|
|
530
|
-
buildCompileRequest(previousAgentOutput) {
|
|
531
|
-
const lcr = makeLoopCompileRequest({
|
|
532
|
-
loop_id: this.config.loopId,
|
|
533
|
-
round: this.currentRound,
|
|
534
|
-
goal_id: this.config.goalId || "",
|
|
535
|
-
task: this.config.task,
|
|
536
|
-
domain: this.config.domain,
|
|
537
|
-
plan_source: this.config.planSource ?? null,
|
|
538
|
-
constraints_from_plan: this.config.constraintsFromPlan,
|
|
539
|
-
health_check_interval: this.config.healthCheckInterval,
|
|
540
|
-
});
|
|
541
|
-
if (this.timedOut && this.currentRound > 1) {
|
|
542
|
-
lcr.force_level = "l2";
|
|
543
|
-
}
|
|
544
|
-
// Attach last round result from previous iteration
|
|
545
|
-
if (previousAgentOutput !== null && this.currentRound > 1) {
|
|
546
|
-
const selfEval = extractSelfEvaluation(previousAgentOutput) ??
|
|
547
|
-
heuristicSelfEvaluation(previousAgentOutput);
|
|
548
|
-
if (selfEval) {
|
|
549
|
-
lcr.last_round_result = makeLoopRoundResult({
|
|
550
|
-
round: this.currentRound - 1,
|
|
551
|
-
success: selfEval.success,
|
|
552
|
-
output_summary: selfEval.output_summary,
|
|
553
|
-
constraint_violations: selfEval.constraint_violations,
|
|
554
|
-
manual_fixes_needed: "",
|
|
555
|
-
// P0–P2: Cognitive evolution
|
|
556
|
-
discovered_constraints: selfEval.discovered_constraints ?? [],
|
|
557
|
-
objective_refinement: selfEval.objective_refinement ?? "",
|
|
558
|
-
emerged_subtasks: selfEval.emerged_subtasks ?? [],
|
|
559
|
-
// P4: Execution evidence
|
|
560
|
-
execution_evidence: selfEval.execution_evidence,
|
|
561
|
-
// P5: Self-correction
|
|
562
|
-
retracted_constraints: selfEval.retracted_constraints ?? [],
|
|
563
|
-
revised_success_criteria: selfEval.revised_success_criteria ?? [],
|
|
564
|
-
wrong_assumptions: selfEval.wrong_assumptions ?? [],
|
|
565
|
-
// v1.10: Checkpoint
|
|
566
|
-
compression_checkpoint: selfEval.compression_checkpoint ?? false,
|
|
567
|
-
checkpoint_label: selfEval.checkpoint_label ?? "",
|
|
568
|
-
// v1.16: Agent's declared next action
|
|
569
|
-
next_action: selfEval.next_action,
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
return lcr;
|
|
574
|
-
}
|
|
575
|
-
/** Adapt the typed compiler request to the engine envelope once for both
|
|
576
|
-
* normal rounds and enforcement retry attempts. */
|
|
577
|
-
buildEngineCompileRequest(previousAgentOutput) {
|
|
578
|
-
const lcr = this.buildCompileRequest(previousAgentOutput);
|
|
579
|
-
return {
|
|
580
|
-
task: this.config.task,
|
|
581
|
-
mode: Mode.LOOP_COMPILE,
|
|
582
|
-
feedback: null,
|
|
583
|
-
skill_name: null,
|
|
584
|
-
task_id: null,
|
|
585
|
-
loop_id: lcr.loop_id,
|
|
586
|
-
round: lcr.round,
|
|
587
|
-
goal_id: lcr.goal_id,
|
|
588
|
-
domain: lcr.domain,
|
|
589
|
-
plan_source: lcr.plan_source,
|
|
590
|
-
constraints_from_plan: lcr.constraints_from_plan,
|
|
591
|
-
health_check_interval: lcr.health_check_interval,
|
|
592
|
-
last_round_result: lcr.last_round_result ?? undefined,
|
|
593
|
-
verification_flags: this.pendingVerificationFlags,
|
|
594
|
-
external_context: this.pendingExternalContext || undefined,
|
|
595
|
-
max_rounds: this.config.maxRounds,
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
// ── Internal: heartbeat ──────────────────────────────────────────────
|
|
599
|
-
startHeartbeat() {
|
|
600
|
-
if (this.config.heartbeatIntervalMs <= 0)
|
|
601
|
-
return;
|
|
602
|
-
this.heartbeatTimer = setInterval(() => {
|
|
603
|
-
this.heartbeatTick();
|
|
604
|
-
}, this.config.heartbeatIntervalMs);
|
|
605
|
-
// Allow the process to exit even if this timer is still running
|
|
606
|
-
if (this.heartbeatTimer && typeof this.heartbeatTimer === "object") {
|
|
607
|
-
this.heartbeatTimer.unref?.();
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
heartbeatTick() {
|
|
611
|
-
if (this._status !== RuntimeStatus.RUNNING)
|
|
612
|
-
return;
|
|
613
|
-
if (this.roundStartTime === 0)
|
|
614
|
-
return;
|
|
615
|
-
try {
|
|
616
|
-
const now = Date.now();
|
|
617
|
-
const elapsed = now - this.roundStartTime;
|
|
618
|
-
const sinceProgress = now - this.lastProgressTime;
|
|
619
|
-
const hbInfo = {
|
|
620
|
-
round: this.currentRound,
|
|
621
|
-
elapsedMs: elapsed,
|
|
622
|
-
sinceProgressMs: sinceProgress,
|
|
623
|
-
};
|
|
624
|
-
this.emit("heartbeat", hbInfo);
|
|
625
|
-
this.config.onHeartbeat?.(hbInfo);
|
|
626
|
-
// Interactive mode: only emit heartbeat, never timeout/stall
|
|
627
|
-
if (this.config.interactive)
|
|
628
|
-
return;
|
|
629
|
-
// Approaching timeout warning (80%)
|
|
630
|
-
if (elapsed > this.config.roundTimeoutMs * 0.8 && !this.timedOut) {
|
|
631
|
-
const warning = {
|
|
632
|
-
type: "approaching_timeout",
|
|
633
|
-
message: `Round ${this.currentRound} approaching timeout ` +
|
|
634
|
-
`(${elapsed}ms / ${this.config.roundTimeoutMs}ms)`,
|
|
635
|
-
};
|
|
636
|
-
this.emit("health:warning", warning);
|
|
637
|
-
this.config.onHealthWarning?.(warning);
|
|
638
|
-
}
|
|
639
|
-
// Timeout
|
|
640
|
-
if (elapsed > this.config.roundTimeoutMs && !this.timedOut) {
|
|
641
|
-
this.triggerTimeout(elapsed);
|
|
642
|
-
}
|
|
643
|
-
// Stall — timeout + grace period elapsed, execute still hasn't returned
|
|
644
|
-
if (this.activeCtx !== null &&
|
|
645
|
-
elapsed > this.config.roundTimeoutMs + this.config.stallGraceMs) {
|
|
646
|
-
this.triggerStall(elapsed);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
catch {
|
|
650
|
-
// heartbeatTick must never throw — silently ignore errors
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
stopHeartbeat() {
|
|
654
|
-
if (this.heartbeatTimer !== null) {
|
|
655
|
-
clearInterval(this.heartbeatTimer);
|
|
656
|
-
this.heartbeatTimer = null;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
// ── Internal: signal handlers ────────────────────────────────────────
|
|
660
|
-
registerSignalHandlers() {
|
|
661
|
-
this.sigintHandler = () => {
|
|
662
|
-
const policy = getPolicy();
|
|
663
|
-
const doubleTapMs = policy.runtime.pause_double_tap_ms;
|
|
664
|
-
const now = Date.now();
|
|
665
|
-
const withinDoubleTapWindow = doubleTapMs > 0 &&
|
|
666
|
-
this.pauseTimestamp > 0 &&
|
|
667
|
-
now - this.pauseTimestamp <= doubleTapMs;
|
|
668
|
-
// First SIGINT requests a boundary pause. A second signal stops only
|
|
669
|
-
// inside the configured window; after it expires, it starts a new window.
|
|
670
|
-
if ((this.pauseRequested || this._status === RuntimeStatus.PAUSED) &&
|
|
671
|
-
withinDoubleTapWindow) {
|
|
672
|
-
this.stop();
|
|
673
|
-
}
|
|
674
|
-
else if (doubleTapMs > 0 && this._status === RuntimeStatus.RUNNING) {
|
|
675
|
-
this.pause();
|
|
676
|
-
}
|
|
677
|
-
else if (doubleTapMs > 0 && this._status === RuntimeStatus.PAUSED) {
|
|
678
|
-
this.pauseTimestamp = now;
|
|
679
|
-
}
|
|
680
|
-
else {
|
|
681
|
-
this.stop();
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
this.sigtermHandler = () => {
|
|
685
|
-
this.stop();
|
|
686
|
-
};
|
|
687
|
-
process.on("SIGINT", this.sigintHandler);
|
|
688
|
-
process.on("SIGTERM", this.sigtermHandler);
|
|
689
|
-
}
|
|
690
|
-
unregisterSignalHandlers() {
|
|
691
|
-
if (this.sigintHandler) {
|
|
692
|
-
process.removeListener("SIGINT", this.sigintHandler);
|
|
693
|
-
this.sigintHandler = null;
|
|
694
|
-
}
|
|
695
|
-
if (this.sigtermHandler) {
|
|
696
|
-
process.removeListener("SIGTERM", this.sigtermHandler);
|
|
697
|
-
this.sigtermHandler = null;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
702
|
-
// Convenience function — the 80% API
|
|
703
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
704
|
-
/** Run a fully autonomous loop. Only `task` and `execute` are required.
|
|
705
|
-
* All other configuration has sensible defaults from `loop_policy.json`.
|
|
706
|
-
*
|
|
707
|
-
* @example
|
|
708
|
-
* const result = await run({
|
|
709
|
-
* task: 'Audit ERC20 token for security vulnerabilities',
|
|
710
|
-
* execute: async (prompt) => await myAgent.call(prompt),
|
|
711
|
-
* });
|
|
712
|
-
* // result: { success, stopReason, roundsCompleted, successTrajectory }
|
|
713
|
-
*/
|
|
714
|
-
export async function run(rawConfig) {
|
|
715
|
-
const runtime = new LoopRuntime(rawConfig);
|
|
716
|
-
return runtime.start();
|
|
717
|
-
}
|
|
718
|
-
//# sourceMappingURL=runtime.js.map
|