loopforge 1.12.0 → 2.0.0
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/README.md +172 -95
- package/dist/backends/interface.d.ts +3 -9
- package/dist/backends/interface.d.ts.map +1 -1
- package/dist/backends/interface.js +1 -5
- package/dist/backends/interface.js.map +1 -1
- package/dist/canonical-state.d.ts +53 -0
- package/dist/canonical-state.d.ts.map +1 -0
- package/dist/canonical-state.js +161 -0
- package/dist/canonical-state.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +227 -0
- package/dist/cli.js.map +1 -0
- package/dist/enforcement-gate.d.ts +44 -0
- package/dist/enforcement-gate.d.ts.map +1 -0
- package/dist/enforcement-gate.js +271 -0
- package/dist/enforcement-gate.js.map +1 -0
- package/dist/engine.d.ts +17 -7
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +158 -249
- package/dist/engine.js.map +1 -1
- package/dist/evidence-provider.d.ts +113 -0
- package/dist/evidence-provider.d.ts.map +1 -0
- package/dist/evidence-provider.js +419 -0
- package/dist/evidence-provider.js.map +1 -0
- package/dist/index.d.ts +19 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -10
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +15 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +49 -0
- package/dist/init.js.map +1 -0
- package/dist/interop.d.ts +30 -0
- package/dist/interop.d.ts.map +1 -0
- package/dist/interop.js +29 -0
- package/dist/interop.js.map +1 -0
- package/dist/loop-compiler.d.ts +14 -64
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +307 -2001
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts +82 -0
- package/dist/loop-store.d.ts.map +1 -0
- package/dist/loop-store.js +332 -0
- package/dist/loop-store.js.map +1 -0
- package/dist/mcp/server.d.ts +9 -6
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +109 -73
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +84 -20
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +718 -316
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +18 -268
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +225 -12
- package/dist/mcp/tools.js.map +1 -1
- package/dist/observability.d.ts +36 -16
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +82 -23
- package/dist/observability.js.map +1 -1
- package/dist/policy-metrics.d.ts +46 -0
- package/dist/policy-metrics.d.ts.map +1 -0
- package/dist/policy-metrics.js +146 -0
- package/dist/policy-metrics.js.map +1 -0
- package/dist/policy.d.ts +58 -75
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +122 -93
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +30 -0
- package/dist/prompt-assembler.d.ts.map +1 -0
- package/dist/prompt-assembler.js +257 -0
- package/dist/prompt-assembler.js.map +1 -0
- package/dist/prompt-policy.d.ts +29 -0
- package/dist/prompt-policy.d.ts.map +1 -0
- package/dist/prompt-policy.js +51 -0
- package/dist/prompt-policy.js.map +1 -0
- package/dist/protocol.d.ts +117 -91
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +40 -36
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +1 -27
- package/dist/replay.js.map +1 -1
- package/dist/round-coordinator.d.ts +90 -0
- package/dist/round-coordinator.d.ts.map +1 -0
- package/dist/round-coordinator.js +186 -0
- package/dist/round-coordinator.js.map +1 -0
- package/dist/round-driver.d.ts +53 -0
- package/dist/round-driver.d.ts.map +1 -0
- package/dist/round-driver.js +101 -0
- package/dist/round-driver.js.map +1 -0
- package/dist/round-transaction.d.ts +70 -0
- package/dist/round-transaction.d.ts.map +1 -0
- package/dist/round-transaction.js +290 -0
- package/dist/round-transaction.js.map +1 -0
- package/dist/runtime.d.ts +35 -17
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +357 -344
- package/dist/runtime.js.map +1 -1
- package/dist/storage.d.ts +38 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +201 -0
- package/dist/storage.js.map +1 -0
- package/dist/verification-gate.d.ts +39 -7
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +198 -9
- package/dist/verification-gate.js.map +1 -1
- package/loop_policy.json +26 -47
- package/package.json +13 -10
- package/skills/perception/SKILL.md +80 -249
- package/dist/backends/fs.d.ts +0 -36
- package/dist/backends/fs.d.ts.map +0 -1
- package/dist/backends/fs.js +0 -358
- package/dist/backends/fs.js.map +0 -1
- package/dist/builder.d.ts +0 -12
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js +0 -259
- package/dist/builder.js.map +0 -1
- package/dist/mcp-server.d.ts +0 -12
- package/dist/mcp-server.d.ts.map +0 -1
- package/dist/mcp-server.js +0 -14
- package/dist/mcp-server.js.map +0 -1
- package/dist/memory-bridge.d.ts +0 -47
- package/dist/memory-bridge.d.ts.map +0 -1
- package/dist/memory-bridge.js +0 -290
- package/dist/memory-bridge.js.map +0 -1
- package/dist/tests/_helpers.d.ts +0 -25
- package/dist/tests/_helpers.d.ts.map +0 -1
- package/dist/tests/_helpers.js +0 -47
- package/dist/tests/_helpers.js.map +0 -1
- package/dist/tests/builder.test.d.ts +0 -2
- package/dist/tests/builder.test.d.ts.map +0 -1
- package/dist/tests/builder.test.js +0 -110
- package/dist/tests/builder.test.js.map +0 -1
- package/dist/tests/engine.test.d.ts +0 -2
- package/dist/tests/engine.test.d.ts.map +0 -1
- package/dist/tests/engine.test.js +0 -371
- package/dist/tests/engine.test.js.map +0 -1
- package/dist/tests/generate-schema.test.d.ts +0 -8
- package/dist/tests/generate-schema.test.d.ts.map +0 -1
- package/dist/tests/generate-schema.test.js +0 -199
- package/dist/tests/generate-schema.test.js.map +0 -1
- package/dist/tests/loop-compiler.test.d.ts +0 -2
- package/dist/tests/loop-compiler.test.d.ts.map +0 -1
- package/dist/tests/loop-compiler.test.js +0 -1051
- package/dist/tests/loop-compiler.test.js.map +0 -1
- package/dist/tests/mcp.test.d.ts +0 -7
- package/dist/tests/mcp.test.d.ts.map +0 -1
- package/dist/tests/mcp.test.js +0 -485
- package/dist/tests/mcp.test.js.map +0 -1
- package/dist/tests/memory-bridge.test.d.ts +0 -8
- package/dist/tests/memory-bridge.test.d.ts.map +0 -1
- package/dist/tests/memory-bridge.test.js +0 -383
- package/dist/tests/memory-bridge.test.js.map +0 -1
- package/dist/tests/policy.test.d.ts +0 -2
- package/dist/tests/policy.test.d.ts.map +0 -1
- package/dist/tests/policy.test.js +0 -49
- package/dist/tests/policy.test.js.map +0 -1
- package/dist/tests/protocol.test.d.ts +0 -2
- package/dist/tests/protocol.test.d.ts.map +0 -1
- package/dist/tests/protocol.test.js +0 -73
- package/dist/tests/protocol.test.js.map +0 -1
- package/dist/tests/replay.test.d.ts +0 -2
- package/dist/tests/replay.test.d.ts.map +0 -1
- package/dist/tests/replay.test.js +0 -222
- package/dist/tests/replay.test.js.map +0 -1
- package/dist/tests/runtime.test.d.ts +0 -7
- package/dist/tests/runtime.test.d.ts.map +0 -1
- package/dist/tests/runtime.test.js +0 -323
- package/dist/tests/runtime.test.js.map +0 -1
- package/dist/tests/verification-gate.test.d.ts +0 -2
- package/dist/tests/verification-gate.test.d.ts.map +0 -1
- package/dist/tests/verification-gate.test.js +0 -407
- package/dist/tests/verification-gate.test.js.map +0 -1
- package/skills/prompt-techniques/SKILL.md +0 -52
- package/skills/prompt-techniques/references/chain-of-thought.md +0 -207
- package/skills/prompt-techniques/references/few-shot.md +0 -174
- package/skills/prompt-techniques/references/least-to-most.md +0 -182
- package/skills/prompt-techniques/references/step-back.md +0 -160
- package/skills/prompt-techniques/references/tree-of-thought.md +0 -191
- package/skills/prompt-techniques/references/zero-shot.md +0 -97
package/dist/runtime.js
CHANGED
|
@@ -14,94 +14,22 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { EventEmitter } from "node:events";
|
|
16
16
|
import { extractSelfEvaluation, heuristicSelfEvaluation, createEngine, } from "./engine.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
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
21
|
import { logEvent } from "./observability.js";
|
|
22
|
-
import {
|
|
22
|
+
import { policyMetrics } from "./policy-metrics.js";
|
|
23
23
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
24
|
// Helpers
|
|
25
25
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
26
|
-
/** Build a writeback payload from the loop's final state.
|
|
27
|
-
* Extracts project-level outcome, tactical lessons, and vault reference.
|
|
28
|
-
* Returns null if there is no meaningful data to write back. */
|
|
29
|
-
function buildWritebackPayload(loopId, task, stopReason, result) {
|
|
30
|
-
// Only write back for meaningful loops (at least 1 round completed)
|
|
31
|
-
if (result.roundsCompleted < 1)
|
|
32
|
-
return null;
|
|
33
|
-
const policy = getPolicy();
|
|
34
|
-
const wp = policy.memory_writeback;
|
|
35
|
-
const outcome = ["completed", "circuit_breaker", "stalled", "max_rounds", "stopped"].find((o) => stopReason === o) ?? "stopped";
|
|
36
|
-
// Build project entry
|
|
37
|
-
const projectEntry = {
|
|
38
|
-
title: `${task.slice(0, 80)} — ${outcome}`,
|
|
39
|
-
objective: task.slice(0, 200),
|
|
40
|
-
keyOutcome: stopReason === "task_complete"
|
|
41
|
-
? `Completed successfully in ${result.roundsCompleted} rounds.`
|
|
42
|
-
: `Terminated with reason '${stopReason}' after ${result.roundsCompleted} rounds.`,
|
|
43
|
-
keyDiscoveries: [],
|
|
44
|
-
date: new Date().toISOString().split("T")[0],
|
|
45
|
-
};
|
|
46
|
-
// Build feedback entries from success trajectory patterns
|
|
47
|
-
const feedbackEntries = [];
|
|
48
|
-
const trajectory = result.successTrajectory;
|
|
49
|
-
if (trajectory.length >= 3) {
|
|
50
|
-
// Detect improving trend via success rate
|
|
51
|
-
const firstHalf = trajectory.slice(0, Math.floor(trajectory.length / 2));
|
|
52
|
-
const secondHalf = trajectory.slice(Math.floor(trajectory.length / 2));
|
|
53
|
-
const firstRate = firstHalf.filter(Boolean).length / firstHalf.length;
|
|
54
|
-
const secondRate = secondHalf.filter(Boolean).length / secondHalf.length;
|
|
55
|
-
if (secondRate - firstRate > 0.3) {
|
|
56
|
-
feedbackEntries.push({
|
|
57
|
-
rule: `Success rate improved from ${(firstRate * 100).toFixed(0)}% to ${(secondRate * 100).toFixed(0)}% over ${trajectory.length} rounds`,
|
|
58
|
-
why: `Mid-loop strategy adjustment unlocked progress in task "${task.slice(0, 80)}"`,
|
|
59
|
-
howToApply: "Consider the strategy used in later rounds for similar tasks",
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Detect flatlining — 3 consecutive failures
|
|
64
|
-
if (trajectory.length >= 3) {
|
|
65
|
-
const recent = trajectory.slice(-3);
|
|
66
|
-
if (recent.every((v) => v === false)) {
|
|
67
|
-
feedbackEntries.push({
|
|
68
|
-
rule: `Failed 3 consecutive rounds at end of loop`,
|
|
69
|
-
why: `Task "${task.slice(0, 80)}" stopped with reason '${stopReason}' — strategy may have stagnated`,
|
|
70
|
-
howToApply: "If re-attempting, start with a different technique than the one that produced the failures",
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
// Build reference entry pointing to vault
|
|
75
|
-
const referenceEntry = {
|
|
76
|
-
description: `LoopForge vault data for "${task.slice(0, 80)}"`,
|
|
77
|
-
vaultLocation: `.promptcraft/prompt_vault.json → loop:${loopId}:*`,
|
|
78
|
-
};
|
|
79
|
-
return {
|
|
80
|
-
loopId,
|
|
81
|
-
task,
|
|
82
|
-
outcome,
|
|
83
|
-
roundsCompleted: result.roundsCompleted,
|
|
84
|
-
projectEntry: {
|
|
85
|
-
...projectEntry,
|
|
86
|
-
keyDiscoveries: projectEntry.keyDiscoveries.slice(0, wp.max_discoveries_in_project),
|
|
87
|
-
},
|
|
88
|
-
feedbackEntries: feedbackEntries.slice(0, wp.max_feedback_entries),
|
|
89
|
-
referenceEntry,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
function autoLoopId(task) {
|
|
93
|
-
let slug = task.toLowerCase().trim().slice(0, 60);
|
|
94
|
-
slug = slug.replace(/[^a-z0-9\s-]/g, "");
|
|
95
|
-
slug = slug.replace(/\s+/g, "-");
|
|
96
|
-
return slug || "unnamed-loop";
|
|
97
|
-
}
|
|
98
26
|
function resolveConfig(raw) {
|
|
99
27
|
const policy = getPolicy();
|
|
100
28
|
const rtPolicy = policy.runtime;
|
|
101
29
|
const config = {
|
|
102
30
|
task: raw.task,
|
|
103
31
|
execute: raw.execute,
|
|
104
|
-
loopId: raw.loopId ||
|
|
32
|
+
loopId: raw.loopId || makeTaskId(raw.task),
|
|
105
33
|
goalId: raw.goalId || "",
|
|
106
34
|
maxRounds: raw.maxRounds ?? rtPolicy.max_rounds,
|
|
107
35
|
roundTimeoutMs: raw.roundTimeoutMs ?? rtPolicy.round_timeout_ms,
|
|
@@ -118,25 +46,25 @@ function resolveConfig(raw) {
|
|
|
118
46
|
onHeartbeat: raw.onHeartbeat,
|
|
119
47
|
onTimeout: raw.onTimeout,
|
|
120
48
|
onHealthWarning: raw.onHealthWarning,
|
|
121
|
-
|
|
122
|
-
|
|
49
|
+
contextProvider: raw.contextProvider,
|
|
50
|
+
terminalSinks: raw.terminalSinks ?? [],
|
|
123
51
|
};
|
|
124
52
|
// Auto-detect claude-mem if no explicit provider was given
|
|
125
|
-
if (!config.memoryProvider && !config.memoryWriter) {
|
|
126
|
-
const auto = tryAutoConfigure();
|
|
127
|
-
if (auto.memoryProvider)
|
|
128
|
-
config.memoryProvider = auto.memoryProvider;
|
|
129
|
-
if (auto.memoryWriter)
|
|
130
|
-
config.memoryWriter = auto.memoryWriter;
|
|
131
|
-
}
|
|
132
53
|
return config;
|
|
133
54
|
}
|
|
55
|
+
class RoundStalledError extends Error {
|
|
56
|
+
constructor() {
|
|
57
|
+
super("Round exceeded timeout and stall grace period");
|
|
58
|
+
this.name = "RoundStalledError";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
134
61
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
135
62
|
// LoopRuntime
|
|
136
63
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
137
64
|
export class LoopRuntime extends EventEmitter {
|
|
138
65
|
config;
|
|
139
66
|
engine;
|
|
67
|
+
roundDriver;
|
|
140
68
|
_status = RuntimeStatus.IDLE;
|
|
141
69
|
currentRound = 1;
|
|
142
70
|
successTrajectory = [];
|
|
@@ -146,22 +74,30 @@ export class LoopRuntime extends EventEmitter {
|
|
|
146
74
|
// Verification gate state (v1.6 unified — was MCP-only, now also in runtime)
|
|
147
75
|
lastSelfEval = null;
|
|
148
76
|
pendingVerificationFlags = [];
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
155
89
|
pendingExternalContext = "";
|
|
156
90
|
// Shared mutable state between the main loop and the heartbeat tick
|
|
157
91
|
roundStartTime = 0;
|
|
158
92
|
lastProgressTime = 0;
|
|
159
93
|
activeCtx = null;
|
|
94
|
+
activeAbortController = null;
|
|
160
95
|
timedOut = false;
|
|
161
96
|
constructor(rawConfig) {
|
|
162
97
|
super();
|
|
163
98
|
this.config = resolveConfig(rawConfig);
|
|
164
99
|
this.engine = createEngine();
|
|
100
|
+
this.roundDriver = new RoundDriver(this.engine);
|
|
165
101
|
}
|
|
166
102
|
get status() {
|
|
167
103
|
return this._status;
|
|
@@ -172,98 +108,10 @@ export class LoopRuntime extends EventEmitter {
|
|
|
172
108
|
getSuccessTrajectory() {
|
|
173
109
|
return [...this.successTrajectory];
|
|
174
110
|
}
|
|
175
|
-
// ── Memory Integration (v1.7) ──────────────────────────────────────────
|
|
176
|
-
/** Derive the current progress estimate from the last self evaluation.
|
|
177
|
-
* Returns -1 if no progress data is available. */
|
|
178
|
-
getCurrentProgress() {
|
|
179
|
-
if (!this.lastSelfEval?.execution_evidence)
|
|
180
|
-
return -1;
|
|
181
|
-
const pe = this.lastSelfEval.execution_evidence.progress_estimate;
|
|
182
|
-
return typeof pe === "number" ? pe : -1;
|
|
183
|
-
}
|
|
184
|
-
/** Determine whether memory should be injected this round based on
|
|
185
|
-
* tier-based allowed phases, progress thresholds, and round spacing. */
|
|
186
|
-
shouldInjectMemory() {
|
|
187
|
-
const policy = getPolicy();
|
|
188
|
-
const mi = policy.memory_injection;
|
|
189
|
-
if (!mi.enabled)
|
|
190
|
-
return false;
|
|
191
|
-
if (!this.config.memoryProvider)
|
|
192
|
-
return false;
|
|
193
|
-
if (this.lastInjectionRound > 0 &&
|
|
194
|
-
this.currentRound - this.lastInjectionRound < mi.min_rounds_between_injections) {
|
|
195
|
-
return false;
|
|
196
|
-
}
|
|
197
|
-
// Resolve which phases are allowed for this loop's maxRounds
|
|
198
|
-
const allowedPhases = new Set(resolveAllowedPhases(this.config.maxRounds, mi.round_tiers));
|
|
199
|
-
const maxInjections = allowedPhases.size;
|
|
200
|
-
if (this.injectionCount >= maxInjections)
|
|
201
|
-
return false;
|
|
202
|
-
const progress = this.getCurrentProgress();
|
|
203
|
-
// Phase 1: round 1, always check if allowed
|
|
204
|
-
if (this.currentRound === 1 && this.injectionCount === 0 && allowedPhases.has(1)) {
|
|
205
|
-
return true;
|
|
206
|
-
}
|
|
207
|
-
// Phase 2: progress threshold (only if allowed by tier)
|
|
208
|
-
if (allowedPhases.has(2) &&
|
|
209
|
-
!this.phase2Triggered &&
|
|
210
|
-
progress >= mi.phase_thresholds.phase2.threshold) {
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
// Phase 3: progress threshold (only if allowed by tier)
|
|
214
|
-
if (allowedPhases.has(3) &&
|
|
215
|
-
!this.phase3Triggered &&
|
|
216
|
-
progress >= mi.phase_thresholds.phase3.threshold) {
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
/** Build the accumulated context for constructing a targeted memory query. */
|
|
222
|
-
buildAccumulatedContext() {
|
|
223
|
-
const recurringIssues = [];
|
|
224
|
-
const failedPatterns = [];
|
|
225
|
-
const keyLessons = [];
|
|
226
|
-
const remainingCriteria = [];
|
|
227
|
-
if (this.lastSelfEval) {
|
|
228
|
-
// Recurring issues: violations that have appeared before
|
|
229
|
-
if (this.lastSelfEval.constraint_violations.length) {
|
|
230
|
-
recurringIssues.push(...this.lastSelfEval.constraint_violations);
|
|
231
|
-
}
|
|
232
|
-
// Remaining criteria
|
|
233
|
-
if (this.lastSelfEval.execution_evidence?.success_criteria_remaining?.length) {
|
|
234
|
-
remainingCriteria.push(...this.lastSelfEval.execution_evidence.success_criteria_remaining);
|
|
235
|
-
}
|
|
236
|
-
// Key lessons from emerged subtasks
|
|
237
|
-
if (this.lastSelfEval.emerged_subtasks?.length) {
|
|
238
|
-
keyLessons.push(...this.lastSelfEval.emerged_subtasks);
|
|
239
|
-
}
|
|
240
|
-
// Wrong assumptions as negative lessons
|
|
241
|
-
if (this.lastSelfEval.wrong_assumptions?.length) {
|
|
242
|
-
keyLessons.push(...this.lastSelfEval.wrong_assumptions.map((a) => `Wrong: ${a}`));
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return { recurringIssues, failedPatterns, keyLessons, remainingCriteria };
|
|
246
|
-
}
|
|
247
|
-
/** Deduplicate external context against previously injected contexts.
|
|
248
|
-
* Returns empty string if the new context is too similar to any prior. */
|
|
249
|
-
dedupAndStoreContext(newContext) {
|
|
250
|
-
if (!newContext.trim())
|
|
251
|
-
return "";
|
|
252
|
-
const policy = getPolicy();
|
|
253
|
-
const threshold = policy.memory_injection.dedup_threshold;
|
|
254
|
-
const newTokens = tokenize(newContext);
|
|
255
|
-
for (const old of this.injectedContexts) {
|
|
256
|
-
const oldTokens = tokenize(old);
|
|
257
|
-
if (jaccard(newTokens, oldTokens) > threshold) {
|
|
258
|
-
return ""; // Too similar — skip
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
this.injectedContexts.push(newContext);
|
|
262
|
-
return newContext;
|
|
263
|
-
}
|
|
264
111
|
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
265
112
|
/** Start the loop. Returns when the loop terminates (task complete,
|
|
266
|
-
* circuit breaker, max rounds, stalled, or manual stop).
|
|
113
|
+
* circuit breaker, max rounds, stalled, paused, or manual stop).
|
|
114
|
+
* Can only be called from IDLE state. */
|
|
267
115
|
async start() {
|
|
268
116
|
if (this._status !== RuntimeStatus.IDLE) {
|
|
269
117
|
throw new Error(`Cannot start: runtime is ${this._status}`);
|
|
@@ -272,122 +120,164 @@ export class LoopRuntime extends EventEmitter {
|
|
|
272
120
|
this.registerSignalHandlers();
|
|
273
121
|
this.startHeartbeat();
|
|
274
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) {
|
|
275
144
|
let stopReason = "max_rounds";
|
|
276
145
|
let previousAgentOutput = null;
|
|
277
146
|
let consecutiveErrors = 0;
|
|
278
147
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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";
|
|
282
157
|
break;
|
|
283
158
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const progress = this.getCurrentProgress();
|
|
288
|
-
let phase;
|
|
289
|
-
if (this.currentRound === 1) {
|
|
290
|
-
phase = 1;
|
|
159
|
+
if (this._status !== RuntimeStatus.RUNNING) {
|
|
160
|
+
if (this._status === RuntimeStatus.PAUSED) {
|
|
161
|
+
stopReason = "paused";
|
|
291
162
|
}
|
|
292
|
-
else if (
|
|
293
|
-
|
|
294
|
-
this.phase2Triggered = true;
|
|
163
|
+
else if (this._status === RuntimeStatus.STALLED) {
|
|
164
|
+
stopReason = "stalled";
|
|
295
165
|
}
|
|
296
166
|
else {
|
|
297
|
-
|
|
298
|
-
this.phase3Triggered = true;
|
|
167
|
+
stopReason = "cancelled";
|
|
299
168
|
}
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
// ── Explicit external context ─────────────────────────────────
|
|
172
|
+
this.pendingExternalContext = "";
|
|
173
|
+
if (this.config.contextProvider) {
|
|
300
174
|
try {
|
|
301
|
-
|
|
175
|
+
this.pendingExternalContext = (await this.config.contextProvider({
|
|
302
176
|
loopId: this.config.loopId,
|
|
303
177
|
round: this.currentRound,
|
|
304
178
|
task: this.config.task,
|
|
305
179
|
domain: this.config.domain,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
accumulatedContext: this.buildAccumulatedContext(),
|
|
309
|
-
};
|
|
310
|
-
const rawContext = await this.config.memoryProvider(ctx);
|
|
311
|
-
this.pendingExternalContext = this.dedupAndStoreContext(rawContext);
|
|
312
|
-
if (this.pendingExternalContext) {
|
|
313
|
-
this.injectionCount++;
|
|
314
|
-
this.lastInjectionRound = this.currentRound;
|
|
315
|
-
logEvent("memory_injected", {
|
|
316
|
-
loopId: this.config.loopId,
|
|
317
|
-
round: this.currentRound,
|
|
318
|
-
phase,
|
|
319
|
-
injectionCount: this.injectionCount,
|
|
320
|
-
contextLength: this.pendingExternalContext.length,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
180
|
+
lastEvaluation: this.lastSelfEval ?? undefined,
|
|
181
|
+
})).trim();
|
|
323
182
|
}
|
|
324
183
|
catch {
|
|
325
|
-
//
|
|
184
|
+
// Provider failures are isolated from loop execution.
|
|
326
185
|
this.pendingExternalContext = "";
|
|
327
|
-
logEvent("
|
|
186
|
+
logEvent("context_provider_error", {
|
|
328
187
|
loopId: this.config.loopId,
|
|
329
188
|
round: this.currentRound,
|
|
330
189
|
});
|
|
331
190
|
}
|
|
332
191
|
}
|
|
333
192
|
// ── Compile ──────────────────────────────────────────────────
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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;
|
|
356
220
|
}
|
|
357
|
-
const prompt = compileResult.response.prompt;
|
|
358
|
-
const analysis = compileResult.response.analysis;
|
|
359
|
-
const level = analysis?.rationale?.includes("l0")
|
|
360
|
-
? "l0"
|
|
361
|
-
: analysis?.rationale?.includes("l1")
|
|
362
|
-
? "l1"
|
|
363
|
-
: "l2";
|
|
364
|
-
const technique = analysis?.technique ?? "unknown";
|
|
365
221
|
// ── Emit round:start ──────────────────────────────────────────
|
|
366
|
-
|
|
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
|
+
};
|
|
367
233
|
this.emit("round:start", startInfo);
|
|
368
234
|
this.config.onRoundStart?.(startInfo);
|
|
369
235
|
// ── Execute ───────────────────────────────────────────────────
|
|
370
236
|
this.roundStartTime = Date.now();
|
|
371
237
|
this.lastProgressTime = this.roundStartTime;
|
|
372
238
|
this.timedOut = false;
|
|
239
|
+
const abortController = new AbortController();
|
|
373
240
|
const ctx = {
|
|
374
241
|
round: this.currentRound,
|
|
375
|
-
|
|
242
|
+
roundId,
|
|
243
|
+
signal: abortController.signal,
|
|
376
244
|
reportProgress: (message) => {
|
|
377
245
|
this.lastProgressTime = Date.now();
|
|
378
246
|
void message; // consumed by heartbeat via lastProgressTime
|
|
379
247
|
},
|
|
380
248
|
};
|
|
381
249
|
this.activeCtx = ctx;
|
|
250
|
+
this.activeAbortController = abortController;
|
|
382
251
|
let agentOutput;
|
|
383
252
|
try {
|
|
384
|
-
agentOutput = await this.
|
|
253
|
+
agentOutput = await this.executeWithDeadline(prompt, ctx);
|
|
385
254
|
consecutiveErrors = 0;
|
|
386
255
|
}
|
|
387
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
|
+
}
|
|
388
277
|
consecutiveErrors++;
|
|
389
278
|
const dur = Date.now() - this.roundStartTime;
|
|
390
279
|
this.activeCtx = null;
|
|
280
|
+
this.activeAbortController = null;
|
|
391
281
|
// Emit round:complete with error info
|
|
392
282
|
const errInfo = {
|
|
393
283
|
round: this.currentRound,
|
|
@@ -407,13 +297,15 @@ export class LoopRuntime extends EventEmitter {
|
|
|
407
297
|
stopReason = "executor_failure";
|
|
408
298
|
break;
|
|
409
299
|
}
|
|
410
|
-
//
|
|
411
|
-
// sees the failure context (forces L1/L2)
|
|
412
|
-
|
|
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`;
|
|
413
304
|
continue;
|
|
414
305
|
}
|
|
415
306
|
const durationMs = Date.now() - this.roundStartTime;
|
|
416
307
|
this.activeCtx = null;
|
|
308
|
+
this.activeAbortController = null;
|
|
417
309
|
// ── Extract self-eval ─────────────────────────────────────────
|
|
418
310
|
let selfEval = extractSelfEvaluation(agentOutput);
|
|
419
311
|
let extractionSucceeded = selfEval !== null;
|
|
@@ -421,40 +313,70 @@ export class LoopRuntime extends EventEmitter {
|
|
|
421
313
|
selfEval = heuristicSelfEvaluation(agentOutput);
|
|
422
314
|
extractionSucceeded = false;
|
|
423
315
|
}
|
|
424
|
-
// ──
|
|
425
|
-
if (selfEval !== null) {
|
|
426
|
-
this.engine.autoFeedback(selfEval, this.config.loopId, this.currentRound, this.config.task);
|
|
427
|
-
}
|
|
428
|
-
// ── Verification gate (v1.6 unified) ──────────────────────────
|
|
429
|
-
// Run cross-round consistency checks between self-eval and lineage.
|
|
430
|
-
// Flags are injected into the NEXT round's prompt via the engine.
|
|
316
|
+
// ── Unified round transaction: evaluate → verify → commit/reject ──
|
|
431
317
|
this.pendingVerificationFlags = [];
|
|
432
|
-
let
|
|
318
|
+
let roundSuccess = selfEval?.success ?? false;
|
|
433
319
|
if (selfEval !== null) {
|
|
434
|
-
const
|
|
435
|
-
|
|
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,
|
|
436
330
|
});
|
|
437
|
-
const
|
|
438
|
-
this.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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;
|
|
446
369
|
}
|
|
447
|
-
this.lastSelfEval = selfEval;
|
|
448
370
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
this.successTrajectory.push(roundSuccess);
|
|
371
|
+
else {
|
|
372
|
+
// Null selfEval: extraction completely failed
|
|
373
|
+
stopReason = "stalled";
|
|
374
|
+
break;
|
|
454
375
|
}
|
|
455
376
|
// ── Emit round:complete ───────────────────────────────────────
|
|
456
377
|
const completeInfo = {
|
|
457
378
|
round: this.currentRound,
|
|
379
|
+
roundId,
|
|
458
380
|
roundSuccess,
|
|
459
381
|
selfEval,
|
|
460
382
|
durationMs,
|
|
@@ -466,22 +388,8 @@ export class LoopRuntime extends EventEmitter {
|
|
|
466
388
|
round: this.currentRound,
|
|
467
389
|
success: roundSuccess,
|
|
468
390
|
durationMs,
|
|
469
|
-
technique: "loop_runtime",
|
|
470
391
|
});
|
|
471
|
-
// ──
|
|
472
|
-
// Extraction check FIRST — heuristic results are low-confidence
|
|
473
|
-
if (!extractionSucceeded) {
|
|
474
|
-
stopReason = "stalled"; // extraction_failure mapped to stalled
|
|
475
|
-
break;
|
|
476
|
-
}
|
|
477
|
-
if (selfEval && !selfEval.should_continue) {
|
|
478
|
-
stopReason = "task_complete";
|
|
479
|
-
break;
|
|
480
|
-
}
|
|
481
|
-
if (this.engine.shouldBreak()) {
|
|
482
|
-
stopReason = "circuit_breaker";
|
|
483
|
-
break;
|
|
484
|
-
}
|
|
392
|
+
// ── Post-round state ──────────────────────────────────────────
|
|
485
393
|
if (this.timedOut) {
|
|
486
394
|
// Next round will be forced L2 via the compile request
|
|
487
395
|
this.timedOut = false;
|
|
@@ -491,14 +399,17 @@ export class LoopRuntime extends EventEmitter {
|
|
|
491
399
|
}
|
|
492
400
|
finally {
|
|
493
401
|
// ── Cleanup — always runs even if the loop body throws ──────────
|
|
494
|
-
|
|
495
|
-
this.
|
|
496
|
-
|
|
497
|
-
this.
|
|
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
|
+
}
|
|
498
409
|
}
|
|
499
410
|
}
|
|
500
411
|
const result = {
|
|
501
|
-
success: stopReason === "
|
|
412
|
+
success: stopReason === "completed",
|
|
502
413
|
stopReason,
|
|
503
414
|
roundsCompleted: this.currentRound > this.config.maxRounds
|
|
504
415
|
? this.config.maxRounds
|
|
@@ -506,40 +417,114 @@ export class LoopRuntime extends EventEmitter {
|
|
|
506
417
|
successTrajectory: [...this.successTrajectory],
|
|
507
418
|
};
|
|
508
419
|
// ── v1.7: Memory Writeback ───────────────────────────────────────
|
|
509
|
-
|
|
510
|
-
if (
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
logEvent("memory_writeback", {
|
|
519
|
-
loopId: this.config.loopId,
|
|
520
|
-
stopReason,
|
|
521
|
-
feedbackCount: payload.feedbackEntries.length,
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
catch {
|
|
526
|
-
// memoryWriter failed — degrade gracefully
|
|
527
|
-
logEvent("memory_writeback_error", {
|
|
528
|
-
loopId: this.config.loopId,
|
|
529
|
-
stopReason,
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
}
|
|
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))));
|
|
533
429
|
}
|
|
534
430
|
this.emit("done", result);
|
|
535
431
|
return result;
|
|
536
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
|
+
}
|
|
537
482
|
/** Stop the loop gracefully. Safe to call from any thread/timer. */
|
|
538
483
|
stop() {
|
|
539
|
-
if (this._status === RuntimeStatus.RUNNING
|
|
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();
|
|
540
489
|
this._status = RuntimeStatus.STOPPED;
|
|
541
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;
|
|
542
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);
|
|
543
528
|
}
|
|
544
529
|
// ── Internal: compile request builder ─────────────────────────────────
|
|
545
530
|
buildCompileRequest(previousAgentOutput) {
|
|
@@ -552,7 +537,6 @@ export class LoopRuntime extends EventEmitter {
|
|
|
552
537
|
plan_source: this.config.planSource ?? null,
|
|
553
538
|
constraints_from_plan: this.config.constraintsFromPlan,
|
|
554
539
|
health_check_interval: this.config.healthCheckInterval,
|
|
555
|
-
vault_config: makeVaultConfig(),
|
|
556
540
|
});
|
|
557
541
|
if (this.timedOut && this.currentRound > 1) {
|
|
558
542
|
lcr.force_level = "l2";
|
|
@@ -581,11 +565,36 @@ export class LoopRuntime extends EventEmitter {
|
|
|
581
565
|
// v1.10: Checkpoint
|
|
582
566
|
compression_checkpoint: selfEval.compression_checkpoint ?? false,
|
|
583
567
|
checkpoint_label: selfEval.checkpoint_label ?? "",
|
|
568
|
+
// v1.16: Agent's declared next action
|
|
569
|
+
next_action: selfEval.next_action,
|
|
584
570
|
});
|
|
585
571
|
}
|
|
586
572
|
}
|
|
587
573
|
return lcr;
|
|
588
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
|
+
}
|
|
589
598
|
// ── Internal: heartbeat ──────────────────────────────────────────────
|
|
590
599
|
startHeartbeat() {
|
|
591
600
|
if (this.config.heartbeatIntervalMs <= 0)
|
|
@@ -629,28 +638,12 @@ export class LoopRuntime extends EventEmitter {
|
|
|
629
638
|
}
|
|
630
639
|
// Timeout
|
|
631
640
|
if (elapsed > this.config.roundTimeoutMs && !this.timedOut) {
|
|
632
|
-
this.
|
|
633
|
-
if (this.activeCtx) {
|
|
634
|
-
this.activeCtx.signal.aborted = true;
|
|
635
|
-
}
|
|
636
|
-
const timeoutInfo = {
|
|
637
|
-
round: this.currentRound,
|
|
638
|
-
elapsedMs: elapsed,
|
|
639
|
-
};
|
|
640
|
-
this.emit("timeout", timeoutInfo);
|
|
641
|
-
this.config.onTimeout?.(timeoutInfo);
|
|
641
|
+
this.triggerTimeout(elapsed);
|
|
642
642
|
}
|
|
643
643
|
// Stall — timeout + grace period elapsed, execute still hasn't returned
|
|
644
644
|
if (this.activeCtx !== null &&
|
|
645
645
|
elapsed > this.config.roundTimeoutMs + this.config.stallGraceMs) {
|
|
646
|
-
this.
|
|
647
|
-
const stallMsg = `Round ${this.currentRound} stalled after ${elapsed}ms`;
|
|
648
|
-
this.emit("stalled", {
|
|
649
|
-
reason: "round_timeout",
|
|
650
|
-
lastRound: this.currentRound,
|
|
651
|
-
elapsedMs: elapsed,
|
|
652
|
-
message: stallMsg,
|
|
653
|
-
});
|
|
646
|
+
this.triggerStall(elapsed);
|
|
654
647
|
}
|
|
655
648
|
}
|
|
656
649
|
catch {
|
|
@@ -666,7 +659,27 @@ export class LoopRuntime extends EventEmitter {
|
|
|
666
659
|
// ── Internal: signal handlers ────────────────────────────────────────
|
|
667
660
|
registerSignalHandlers() {
|
|
668
661
|
this.sigintHandler = () => {
|
|
669
|
-
|
|
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
|
+
}
|
|
670
683
|
};
|
|
671
684
|
this.sigtermHandler = () => {
|
|
672
685
|
this.stop();
|