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.
Files changed (72) hide show
  1. package/dist/backends/interface.d.ts +4 -3
  2. package/dist/backends/interface.d.ts.map +1 -1
  3. package/dist/backends/interface.js +4 -1
  4. package/dist/backends/interface.js.map +1 -1
  5. package/dist/cli.js +1 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/engine.d.ts +2 -24
  8. package/dist/engine.d.ts.map +1 -1
  9. package/dist/engine.js +72 -238
  10. package/dist/engine.js.map +1 -1
  11. package/dist/evidence-provider.d.ts +38 -5
  12. package/dist/evidence-provider.d.ts.map +1 -1
  13. package/dist/evidence-provider.js +131 -38
  14. package/dist/evidence-provider.js.map +1 -1
  15. package/dist/index.d.ts +3 -5
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -4
  18. package/dist/index.js.map +1 -1
  19. package/dist/interop.d.ts +5 -0
  20. package/dist/interop.d.ts.map +1 -1
  21. package/dist/interop.js +5 -0
  22. package/dist/interop.js.map +1 -1
  23. package/dist/loop-compiler.d.ts.map +1 -1
  24. package/dist/loop-compiler.js +4 -2
  25. package/dist/loop-compiler.js.map +1 -1
  26. package/dist/loop-extras-parser.d.ts +82 -0
  27. package/dist/loop-extras-parser.d.ts.map +1 -0
  28. package/dist/loop-extras-parser.js +217 -0
  29. package/dist/loop-extras-parser.js.map +1 -0
  30. package/dist/loop-store.d.ts +26 -4
  31. package/dist/loop-store.d.ts.map +1 -1
  32. package/dist/loop-store.js +96 -7
  33. package/dist/loop-store.js.map +1 -1
  34. package/dist/mcp/server.js +1 -1
  35. package/dist/mcp/session.d.ts +27 -2
  36. package/dist/mcp/session.d.ts.map +1 -1
  37. package/dist/mcp/session.js +166 -117
  38. package/dist/mcp/session.js.map +1 -1
  39. package/dist/mcp/tools.d.ts.map +1 -1
  40. package/dist/mcp/tools.js +1 -0
  41. package/dist/mcp/tools.js.map +1 -1
  42. package/dist/policy.d.ts +0 -8
  43. package/dist/policy.d.ts.map +1 -1
  44. package/dist/policy.js +1 -9
  45. package/dist/policy.js.map +1 -1
  46. package/dist/prompt-assembler.d.ts.map +1 -1
  47. package/dist/prompt-assembler.js +25 -4
  48. package/dist/prompt-assembler.js.map +1 -1
  49. package/dist/protocol.d.ts +2 -74
  50. package/dist/protocol.d.ts.map +1 -1
  51. package/dist/protocol.js +0 -26
  52. package/dist/protocol.js.map +1 -1
  53. package/dist/round-coordinator.d.ts +4 -4
  54. package/dist/round-coordinator.js +4 -4
  55. package/dist/self-eval.d.ts +26 -0
  56. package/dist/self-eval.d.ts.map +1 -0
  57. package/dist/self-eval.js +154 -0
  58. package/dist/self-eval.js.map +1 -0
  59. package/dist/storage.d.ts +3 -2
  60. package/dist/storage.d.ts.map +1 -1
  61. package/dist/storage.js +57 -65
  62. package/dist/storage.js.map +1 -1
  63. package/dist/verification-gate.d.ts +16 -18
  64. package/dist/verification-gate.d.ts.map +1 -1
  65. package/dist/verification-gate.js +149 -94
  66. package/dist/verification-gate.js.map +1 -1
  67. package/loop_policy.json +1 -9
  68. package/package.json +1 -1
  69. package/dist/runtime.d.ts +0 -94
  70. package/dist/runtime.d.ts.map +0 -1
  71. package/dist/runtime.js +0 -718
  72. 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