loopforge 1.15.1 → 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.map +1 -1
- package/dist/enforcement-gate.js +28 -10
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +7 -4
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +78 -138
- 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 +18 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -9
- 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 +303 -2206
- 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 +75 -19
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +707 -361
- 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 +53 -140
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +119 -184
- 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 +88 -93
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +23 -39
- 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 +33 -19
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +355 -336
- 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 +33 -7
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +195 -7
- package/dist/verification-gate.js.map +1 -1
- package/loop_policy.json +22 -48
- package/package.json +13 -10
- package/skills/perception/SKILL.md +80 -267
- package/dist/backends/fs.d.ts +0 -36
- package/dist/backends/fs.d.ts.map +0 -1
- package/dist/backends/fs.js +0 -362
- package/dist/backends/fs.js.map +0 -1
- package/dist/builder.d.ts +0 -19
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js +0 -215
- 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 -112
- package/dist/tests/builder.test.js.map +0 -1
- package/dist/tests/enforcement-gate.test.d.ts +0 -2
- package/dist/tests/enforcement-gate.test.d.ts.map +0 -1
- package/dist/tests/enforcement-gate.test.js +0 -328
- package/dist/tests/enforcement-gate.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 -375
- 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 -1059
- 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 -505
- 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 -337
- 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
|
@@ -13,73 +13,23 @@
|
|
|
13
13
|
* const result = await rt.start();
|
|
14
14
|
*/
|
|
15
15
|
import { EventEmitter } from "node:events";
|
|
16
|
-
import { writeFileSync, mkdirSync } from "node:fs";
|
|
17
|
-
import { join } from "node:path";
|
|
18
16
|
import { extractSelfEvaluation, heuristicSelfEvaluation, createEngine, } from "./engine.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { enforceRound, buildRejectionPrompt } from "./enforcement-gate.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";
|
|
24
21
|
import { logEvent } from "./observability.js";
|
|
25
|
-
import {
|
|
22
|
+
import { policyMetrics } from "./policy-metrics.js";
|
|
26
23
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
27
24
|
// Helpers
|
|
28
25
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
-
/** Build a writeback payload from the loop's final state.
|
|
30
|
-
* Uses shared base builder from policy.ts, then layers runtime-specific
|
|
31
|
-
* feedback entries from trajectory analysis on top. */
|
|
32
|
-
function buildWritebackPayload(loopId, task, stopReason, result, discoveries = []) {
|
|
33
|
-
if (result.roundsCompleted < 1)
|
|
34
|
-
return null;
|
|
35
|
-
const wp = getPolicy().memory_writeback;
|
|
36
|
-
const base = buildBaseMemoryWriteback({
|
|
37
|
-
loopId, task, stopReason, roundsCompleted: result.roundsCompleted, discoveries,
|
|
38
|
-
});
|
|
39
|
-
// Build feedback entries from success trajectory patterns (runtime-specific)
|
|
40
|
-
const feedbackEntries = [];
|
|
41
|
-
const trajectory = result.successTrajectory;
|
|
42
|
-
if (trajectory.length >= 3) {
|
|
43
|
-
const firstHalf = trajectory.slice(0, Math.floor(trajectory.length / 2));
|
|
44
|
-
const secondHalf = trajectory.slice(Math.floor(trajectory.length / 2));
|
|
45
|
-
const firstRate = firstHalf.filter(Boolean).length / firstHalf.length;
|
|
46
|
-
const secondRate = secondHalf.filter(Boolean).length / secondHalf.length;
|
|
47
|
-
if (secondRate - firstRate > 0.3) {
|
|
48
|
-
feedbackEntries.push({
|
|
49
|
-
rule: `Success rate improved from ${(firstRate * 100).toFixed(0)}% to ${(secondRate * 100).toFixed(0)}% over ${trajectory.length} rounds`,
|
|
50
|
-
why: `Mid-loop strategy adjustment unlocked progress in task "${task.slice(0, 80)}"`,
|
|
51
|
-
howToApply: "Consider the strategy used in later rounds for similar tasks",
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if (trajectory.length >= 3) {
|
|
56
|
-
const recent = trajectory.slice(-3);
|
|
57
|
-
if (recent.every((v) => v === false)) {
|
|
58
|
-
feedbackEntries.push({
|
|
59
|
-
rule: `Failed 3 consecutive rounds at end of loop`,
|
|
60
|
-
why: `Task "${task.slice(0, 80)}" stopped with reason '${stopReason}' — strategy may have stagnated`,
|
|
61
|
-
howToApply: "If re-attempting, start with a different technique than the one that produced the failures",
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
...base,
|
|
67
|
-
feedbackEntries: feedbackEntries.slice(0, wp.max_feedback_entries),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
function autoLoopId(task) {
|
|
71
|
-
let slug = task.toLowerCase().trim().slice(0, 60);
|
|
72
|
-
slug = slug.replace(/[^a-z0-9\s-]/g, "");
|
|
73
|
-
slug = slug.replace(/\s+/g, "-");
|
|
74
|
-
return slug || "unnamed-loop";
|
|
75
|
-
}
|
|
76
26
|
function resolveConfig(raw) {
|
|
77
27
|
const policy = getPolicy();
|
|
78
28
|
const rtPolicy = policy.runtime;
|
|
79
29
|
const config = {
|
|
80
30
|
task: raw.task,
|
|
81
31
|
execute: raw.execute,
|
|
82
|
-
loopId: raw.loopId ||
|
|
32
|
+
loopId: raw.loopId || makeTaskId(raw.task),
|
|
83
33
|
goalId: raw.goalId || "",
|
|
84
34
|
maxRounds: raw.maxRounds ?? rtPolicy.max_rounds,
|
|
85
35
|
roundTimeoutMs: raw.roundTimeoutMs ?? rtPolicy.round_timeout_ms,
|
|
@@ -96,25 +46,25 @@ function resolveConfig(raw) {
|
|
|
96
46
|
onHeartbeat: raw.onHeartbeat,
|
|
97
47
|
onTimeout: raw.onTimeout,
|
|
98
48
|
onHealthWarning: raw.onHealthWarning,
|
|
99
|
-
|
|
100
|
-
|
|
49
|
+
contextProvider: raw.contextProvider,
|
|
50
|
+
terminalSinks: raw.terminalSinks ?? [],
|
|
101
51
|
};
|
|
102
52
|
// Auto-detect claude-mem if no explicit provider was given
|
|
103
|
-
if (!config.memoryProvider && !config.memoryWriter) {
|
|
104
|
-
const auto = tryAutoConfigure();
|
|
105
|
-
if (auto.memoryProvider)
|
|
106
|
-
config.memoryProvider = auto.memoryProvider;
|
|
107
|
-
if (auto.memoryWriter)
|
|
108
|
-
config.memoryWriter = auto.memoryWriter;
|
|
109
|
-
}
|
|
110
53
|
return config;
|
|
111
54
|
}
|
|
55
|
+
class RoundStalledError extends Error {
|
|
56
|
+
constructor() {
|
|
57
|
+
super("Round exceeded timeout and stall grace period");
|
|
58
|
+
this.name = "RoundStalledError";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
112
61
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
113
62
|
// LoopRuntime
|
|
114
63
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
115
64
|
export class LoopRuntime extends EventEmitter {
|
|
116
65
|
config;
|
|
117
66
|
engine;
|
|
67
|
+
roundDriver;
|
|
118
68
|
_status = RuntimeStatus.IDLE;
|
|
119
69
|
currentRound = 1;
|
|
120
70
|
successTrajectory = [];
|
|
@@ -126,23 +76,28 @@ export class LoopRuntime extends EventEmitter {
|
|
|
126
76
|
pendingVerificationFlags = [];
|
|
127
77
|
// Enforcement gate state (v1.13)
|
|
128
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 = "";
|
|
129
82
|
pendingRejectionNotice = "";
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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.
|
|
136
89
|
pendingExternalContext = "";
|
|
137
90
|
// Shared mutable state between the main loop and the heartbeat tick
|
|
138
91
|
roundStartTime = 0;
|
|
139
92
|
lastProgressTime = 0;
|
|
140
93
|
activeCtx = null;
|
|
94
|
+
activeAbortController = null;
|
|
141
95
|
timedOut = false;
|
|
142
96
|
constructor(rawConfig) {
|
|
143
97
|
super();
|
|
144
98
|
this.config = resolveConfig(rawConfig);
|
|
145
99
|
this.engine = createEngine();
|
|
100
|
+
this.roundDriver = new RoundDriver(this.engine);
|
|
146
101
|
}
|
|
147
102
|
get status() {
|
|
148
103
|
return this._status;
|
|
@@ -153,64 +108,10 @@ export class LoopRuntime extends EventEmitter {
|
|
|
153
108
|
getSuccessTrajectory() {
|
|
154
109
|
return [...this.successTrajectory];
|
|
155
110
|
}
|
|
156
|
-
// ── Memory Integration (v1.7) ──────────────────────────────────────────
|
|
157
|
-
/** Derive the current progress estimate from the last self evaluation.
|
|
158
|
-
* Returns -1 if no progress data is available. */
|
|
159
|
-
getCurrentProgress() {
|
|
160
|
-
if (!this.lastSelfEval?.execution_evidence)
|
|
161
|
-
return -1;
|
|
162
|
-
const pe = this.lastSelfEval.execution_evidence.progress_estimate;
|
|
163
|
-
return typeof pe === "number" ? pe : -1;
|
|
164
|
-
}
|
|
165
|
-
/** Determine which memory injection phase (0/1/2/3) should fire this round.
|
|
166
|
-
* Returns 0 if no injection should occur. Phase tracking (phase2Triggered /
|
|
167
|
-
* phase3Triggered) is updated by the caller based on the returned phase. */
|
|
168
|
-
getInjectionPhase() {
|
|
169
|
-
const policy = getPolicy();
|
|
170
|
-
const mi = policy.memory_injection;
|
|
171
|
-
if (!mi.enabled)
|
|
172
|
-
return 0;
|
|
173
|
-
if (!this.config.memoryProvider)
|
|
174
|
-
return 0;
|
|
175
|
-
if (this.lastInjectionRound > 0 &&
|
|
176
|
-
this.currentRound - this.lastInjectionRound < mi.min_rounds_between_injections) {
|
|
177
|
-
return 0;
|
|
178
|
-
}
|
|
179
|
-
const allowedPhases = new Set(resolveAllowedPhases(this.config.maxRounds, mi.round_tiers));
|
|
180
|
-
const progress = this.getCurrentProgress();
|
|
181
|
-
return resolveInjectionPhase(this.currentRound, this.injectionCount, allowedPhases, progress, this.phase2Triggered, this.phase3Triggered, {
|
|
182
|
-
phase2: mi.phase_thresholds.phase2.threshold,
|
|
183
|
-
phase3: mi.phase_thresholds.phase3.threshold,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
/** Build the accumulated context for constructing a targeted memory query.
|
|
187
|
-
* Delegates to the shared buildAccumulatedMemoryContext() utility. */
|
|
188
|
-
buildAccumulatedContext() {
|
|
189
|
-
if (!this.lastSelfEval) {
|
|
190
|
-
return { recurringIssues: [], failedPatterns: [], keyLessons: [], remainingCriteria: [] };
|
|
191
|
-
}
|
|
192
|
-
return buildAccumulatedMemoryContext(this.lastSelfEval);
|
|
193
|
-
}
|
|
194
|
-
/** Deduplicate external context against previously injected contexts.
|
|
195
|
-
* Returns empty string if the new context is too similar to any prior. */
|
|
196
|
-
dedupAndStoreContext(newContext) {
|
|
197
|
-
if (!newContext.trim())
|
|
198
|
-
return "";
|
|
199
|
-
const policy = getPolicy();
|
|
200
|
-
const threshold = policy.memory_injection.dedup_threshold;
|
|
201
|
-
const newTokens = tokenize(newContext);
|
|
202
|
-
for (const old of this.injectedContexts) {
|
|
203
|
-
const oldTokens = tokenize(old);
|
|
204
|
-
if (jaccard(newTokens, oldTokens) > threshold) {
|
|
205
|
-
return ""; // Too similar — skip
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
this.injectedContexts.push(newContext);
|
|
209
|
-
return newContext;
|
|
210
|
-
}
|
|
211
111
|
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
212
112
|
/** Start the loop. Returns when the loop terminates (task complete,
|
|
213
|
-
* 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. */
|
|
214
115
|
async start() {
|
|
215
116
|
if (this._status !== RuntimeStatus.IDLE) {
|
|
216
117
|
throw new Error(`Cannot start: runtime is ${this._status}`);
|
|
@@ -219,130 +120,164 @@ export class LoopRuntime extends EventEmitter {
|
|
|
219
120
|
this.registerSignalHandlers();
|
|
220
121
|
this.startHeartbeat();
|
|
221
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) {
|
|
222
144
|
let stopReason = "max_rounds";
|
|
223
145
|
let previousAgentOutput = null;
|
|
224
146
|
let consecutiveErrors = 0;
|
|
225
147
|
try {
|
|
226
|
-
|
|
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
|
+
}
|
|
227
159
|
if (this._status !== RuntimeStatus.RUNNING) {
|
|
228
|
-
|
|
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
|
+
}
|
|
229
169
|
break;
|
|
230
170
|
}
|
|
231
|
-
// ──
|
|
171
|
+
// ── Explicit external context ─────────────────────────────────
|
|
232
172
|
this.pendingExternalContext = "";
|
|
233
|
-
|
|
234
|
-
if (injectionPhase !== 0) {
|
|
235
|
-
// Track phase triggers
|
|
236
|
-
if (injectionPhase === 2)
|
|
237
|
-
this.phase2Triggered = true;
|
|
238
|
-
if (injectionPhase === 3)
|
|
239
|
-
this.phase3Triggered = true;
|
|
240
|
-
const progress = this.getCurrentProgress();
|
|
173
|
+
if (this.config.contextProvider) {
|
|
241
174
|
try {
|
|
242
|
-
|
|
175
|
+
this.pendingExternalContext = (await this.config.contextProvider({
|
|
243
176
|
loopId: this.config.loopId,
|
|
244
177
|
round: this.currentRound,
|
|
245
178
|
task: this.config.task,
|
|
246
179
|
domain: this.config.domain,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
accumulatedContext: this.buildAccumulatedContext(),
|
|
250
|
-
};
|
|
251
|
-
const rawContext = await this.config.memoryProvider(ctx);
|
|
252
|
-
this.pendingExternalContext = this.dedupAndStoreContext(rawContext);
|
|
253
|
-
if (this.pendingExternalContext) {
|
|
254
|
-
this.injectionCount++;
|
|
255
|
-
this.lastInjectionRound = this.currentRound;
|
|
256
|
-
logEvent("memory_injected", {
|
|
257
|
-
loopId: this.config.loopId,
|
|
258
|
-
round: this.currentRound,
|
|
259
|
-
phase: injectionPhase,
|
|
260
|
-
injectionCount: this.injectionCount,
|
|
261
|
-
contextLength: this.pendingExternalContext.length,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
180
|
+
lastEvaluation: this.lastSelfEval ?? undefined,
|
|
181
|
+
})).trim();
|
|
264
182
|
}
|
|
265
183
|
catch {
|
|
266
|
-
//
|
|
184
|
+
// Provider failures are isolated from loop execution.
|
|
267
185
|
this.pendingExternalContext = "";
|
|
268
|
-
logEvent("
|
|
186
|
+
logEvent("context_provider_error", {
|
|
269
187
|
loopId: this.config.loopId,
|
|
270
188
|
round: this.currentRound,
|
|
271
189
|
});
|
|
272
190
|
}
|
|
273
191
|
}
|
|
274
192
|
// ── Compile ──────────────────────────────────────────────────
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
task: this.config.task,
|
|
278
|
-
mode: Mode.LOOP_COMPILE,
|
|
279
|
-
vault_config: makeVaultConfig(),
|
|
280
|
-
feedback: null,
|
|
281
|
-
skill_name: null,
|
|
282
|
-
task_id: null,
|
|
283
|
-
loop_id: lcr.loop_id,
|
|
284
|
-
round: lcr.round,
|
|
285
|
-
goal_id: lcr.goal_id,
|
|
286
|
-
domain: lcr.domain ?? "",
|
|
287
|
-
plan_source: lcr.plan_source ?? null,
|
|
288
|
-
constraints_from_plan: lcr.constraints_from_plan ?? [],
|
|
289
|
-
health_check_interval: lcr.health_check_interval,
|
|
290
|
-
last_round_result: (lcr.last_round_result ?? undefined),
|
|
291
|
-
verification_flags: this.pendingVerificationFlags,
|
|
292
|
-
external_context: this.pendingExternalContext || undefined,
|
|
293
|
-
max_rounds: this.config.maxRounds,
|
|
294
|
-
});
|
|
295
|
-
// v1.14: Write state file if the compiler produced one and policy allows it
|
|
296
|
-
const sfContent = compileResult.response?.state_file_content;
|
|
297
|
-
if (sfContent && getPolicy().state_file.enabled) {
|
|
298
|
-
const sfDir = join(process.cwd(), getPolicy().state_file.directory);
|
|
299
|
-
mkdirSync(sfDir, { recursive: true });
|
|
300
|
-
writeFileSync(join(sfDir, `${this.config.loopId}-state.md`), sfContent, "utf-8");
|
|
301
|
-
}
|
|
302
|
-
if (!compileResult.response?.prompt) {
|
|
303
|
-
stopReason = "stalled";
|
|
304
|
-
break;
|
|
305
|
-
}
|
|
306
|
-
let prompt = compileResult.response.prompt;
|
|
307
|
-
// If this is a redo of a rejected round, use ONLY the rejection prompt.
|
|
308
|
-
// The agent needs to fix a specific issue, not get a fresh compilation.
|
|
309
|
-
// Normal compilation resumes on the next round after enforcement accepts.
|
|
193
|
+
let prompt;
|
|
194
|
+
let level;
|
|
310
195
|
if (this.pendingRejectionNotice) {
|
|
311
|
-
|
|
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;
|
|
312
220
|
}
|
|
313
|
-
const analysis = compileResult.response.analysis;
|
|
314
|
-
const level = analysis?.rationale?.includes("l0")
|
|
315
|
-
? "l0"
|
|
316
|
-
: analysis?.rationale?.includes("l1")
|
|
317
|
-
? "l1"
|
|
318
|
-
: "l2";
|
|
319
|
-
const technique = analysis?.technique ?? "unknown";
|
|
320
221
|
// ── Emit round:start ──────────────────────────────────────────
|
|
321
|
-
|
|
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
|
+
};
|
|
322
233
|
this.emit("round:start", startInfo);
|
|
323
234
|
this.config.onRoundStart?.(startInfo);
|
|
324
235
|
// ── Execute ───────────────────────────────────────────────────
|
|
325
236
|
this.roundStartTime = Date.now();
|
|
326
237
|
this.lastProgressTime = this.roundStartTime;
|
|
327
238
|
this.timedOut = false;
|
|
239
|
+
const abortController = new AbortController();
|
|
328
240
|
const ctx = {
|
|
329
241
|
round: this.currentRound,
|
|
330
|
-
|
|
242
|
+
roundId,
|
|
243
|
+
signal: abortController.signal,
|
|
331
244
|
reportProgress: (message) => {
|
|
332
245
|
this.lastProgressTime = Date.now();
|
|
333
246
|
void message; // consumed by heartbeat via lastProgressTime
|
|
334
247
|
},
|
|
335
248
|
};
|
|
336
249
|
this.activeCtx = ctx;
|
|
250
|
+
this.activeAbortController = abortController;
|
|
337
251
|
let agentOutput;
|
|
338
252
|
try {
|
|
339
|
-
agentOutput = await this.
|
|
253
|
+
agentOutput = await this.executeWithDeadline(prompt, ctx);
|
|
340
254
|
consecutiveErrors = 0;
|
|
341
255
|
}
|
|
342
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
|
+
}
|
|
343
277
|
consecutiveErrors++;
|
|
344
278
|
const dur = Date.now() - this.roundStartTime;
|
|
345
279
|
this.activeCtx = null;
|
|
280
|
+
this.activeAbortController = null;
|
|
346
281
|
// Emit round:complete with error info
|
|
347
282
|
const errInfo = {
|
|
348
283
|
round: this.currentRound,
|
|
@@ -362,13 +297,15 @@ export class LoopRuntime extends EventEmitter {
|
|
|
362
297
|
stopReason = "executor_failure";
|
|
363
298
|
break;
|
|
364
299
|
}
|
|
365
|
-
//
|
|
366
|
-
// sees the failure context (forces L1/L2)
|
|
367
|
-
|
|
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`;
|
|
368
304
|
continue;
|
|
369
305
|
}
|
|
370
306
|
const durationMs = Date.now() - this.roundStartTime;
|
|
371
307
|
this.activeCtx = null;
|
|
308
|
+
this.activeAbortController = null;
|
|
372
309
|
// ── Extract self-eval ─────────────────────────────────────────
|
|
373
310
|
let selfEval = extractSelfEvaluation(agentOutput);
|
|
374
311
|
let extractionSucceeded = selfEval !== null;
|
|
@@ -376,71 +313,70 @@ export class LoopRuntime extends EventEmitter {
|
|
|
376
313
|
selfEval = heuristicSelfEvaluation(agentOutput);
|
|
377
314
|
extractionSucceeded = false;
|
|
378
315
|
}
|
|
379
|
-
// ──
|
|
380
|
-
// Run BEFORE autoFeedback so rejected rounds don't pollute vault.
|
|
316
|
+
// ── Unified round transaction: evaluate → verify → commit/reject ──
|
|
381
317
|
this.pendingVerificationFlags = [];
|
|
382
|
-
let
|
|
318
|
+
let roundSuccess = selfEval?.success ?? false;
|
|
383
319
|
if (selfEval !== null) {
|
|
384
|
-
const
|
|
385
|
-
|
|
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,
|
|
386
330
|
});
|
|
387
|
-
const
|
|
388
|
-
this.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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;
|
|
396
369
|
}
|
|
397
|
-
// ── Enforcement gate (v1.13) — before autoFeedback ──────────
|
|
398
|
-
// Only run for structured self-evals, not heuristic fallback.
|
|
399
|
-
if (extractionSucceeded) {
|
|
400
|
-
const enforceResult = enforceRound(selfEval, verifyResult, this.currentRound, vaultEntries, this.consecutiveRejections);
|
|
401
|
-
if (enforceResult.action === "reject") {
|
|
402
|
-
this.consecutiveRejections++;
|
|
403
|
-
this.pendingRejectionNotice = buildRejectionPrompt(this.currentRound, this.config.task, enforceResult);
|
|
404
|
-
logEvent("enforcement_reject", {
|
|
405
|
-
loopId: this.config.loopId,
|
|
406
|
-
round: this.currentRound,
|
|
407
|
-
reason: enforceResult.reason.slice(0, 120),
|
|
408
|
-
consecutiveRejections: this.consecutiveRejections,
|
|
409
|
-
});
|
|
410
|
-
// Don't update lastSelfEval or successTrajectory.
|
|
411
|
-
// Decrement currentRound so the for-loop increment returns
|
|
412
|
-
// to the same round for the retry.
|
|
413
|
-
this.currentRound--;
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
if (enforceResult.action === "terminate") {
|
|
417
|
-
stopReason = "enforcement_terminated";
|
|
418
|
-
logEvent("session_end", {
|
|
419
|
-
loopId: this.config.loopId,
|
|
420
|
-
stopReason: "enforcement_terminated",
|
|
421
|
-
round: this.currentRound,
|
|
422
|
-
});
|
|
423
|
-
break;
|
|
424
|
-
}
|
|
425
|
-
// Accept: reset rejection counter
|
|
426
|
-
this.consecutiveRejections = 0;
|
|
427
|
-
this.pendingRejectionNotice = "";
|
|
428
|
-
} // end if (extractionSucceeded)
|
|
429
|
-
this.lastSelfEval = selfEval;
|
|
430
|
-
}
|
|
431
|
-
// ── Auto-feedback (AFTER enforcement, only if accepted) ─────────
|
|
432
|
-
if (selfEval !== null) {
|
|
433
|
-
this.engine.autoFeedback(selfEval, this.config.loopId, this.currentRound, this.config.task);
|
|
434
370
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
this.successTrajectory.push(roundSuccess);
|
|
371
|
+
else {
|
|
372
|
+
// Null selfEval: extraction completely failed
|
|
373
|
+
stopReason = "stalled";
|
|
374
|
+
break;
|
|
440
375
|
}
|
|
441
376
|
// ── Emit round:complete ───────────────────────────────────────
|
|
442
377
|
const completeInfo = {
|
|
443
378
|
round: this.currentRound,
|
|
379
|
+
roundId,
|
|
444
380
|
roundSuccess,
|
|
445
381
|
selfEval,
|
|
446
382
|
durationMs,
|
|
@@ -452,22 +388,8 @@ export class LoopRuntime extends EventEmitter {
|
|
|
452
388
|
round: this.currentRound,
|
|
453
389
|
success: roundSuccess,
|
|
454
390
|
durationMs,
|
|
455
|
-
technique: "loop_runtime",
|
|
456
391
|
});
|
|
457
|
-
// ──
|
|
458
|
-
// Extraction check FIRST — heuristic results are low-confidence
|
|
459
|
-
if (!extractionSucceeded) {
|
|
460
|
-
stopReason = "stalled"; // extraction_failure mapped to stalled
|
|
461
|
-
break;
|
|
462
|
-
}
|
|
463
|
-
if (selfEval && !selfEval.should_continue) {
|
|
464
|
-
stopReason = "task_complete";
|
|
465
|
-
break;
|
|
466
|
-
}
|
|
467
|
-
if (this.engine.shouldBreak()) {
|
|
468
|
-
stopReason = "circuit_breaker";
|
|
469
|
-
break;
|
|
470
|
-
}
|
|
392
|
+
// ── Post-round state ──────────────────────────────────────────
|
|
471
393
|
if (this.timedOut) {
|
|
472
394
|
// Next round will be forced L2 via the compile request
|
|
473
395
|
this.timedOut = false;
|
|
@@ -477,14 +399,17 @@ export class LoopRuntime extends EventEmitter {
|
|
|
477
399
|
}
|
|
478
400
|
finally {
|
|
479
401
|
// ── Cleanup — always runs even if the loop body throws ──────────
|
|
480
|
-
|
|
481
|
-
this.
|
|
482
|
-
|
|
483
|
-
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
|
+
}
|
|
484
409
|
}
|
|
485
410
|
}
|
|
486
411
|
const result = {
|
|
487
|
-
success: stopReason === "
|
|
412
|
+
success: stopReason === "completed",
|
|
488
413
|
stopReason,
|
|
489
414
|
roundsCompleted: this.currentRound > this.config.maxRounds
|
|
490
415
|
? this.config.maxRounds
|
|
@@ -492,48 +417,114 @@ export class LoopRuntime extends EventEmitter {
|
|
|
492
417
|
successTrajectory: [...this.successTrajectory],
|
|
493
418
|
};
|
|
494
419
|
// ── v1.7: Memory Writeback ───────────────────────────────────────
|
|
495
|
-
|
|
496
|
-
if (
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
...(lastEval.wrong_assumptions ?? []),
|
|
505
|
-
...(lastEval.emerged_subtasks ?? []),
|
|
506
|
-
...(lastEval.discovered_constraints ?? []),
|
|
507
|
-
]
|
|
508
|
-
: [];
|
|
509
|
-
const payload = buildWritebackPayload(this.config.loopId, this.config.task, stopReason, result, discoveries);
|
|
510
|
-
if (payload) {
|
|
511
|
-
await this.config.memoryWriter(payload);
|
|
512
|
-
logEvent("memory_writeback", {
|
|
513
|
-
loopId: this.config.loopId,
|
|
514
|
-
stopReason,
|
|
515
|
-
feedbackCount: payload.feedbackEntries.length,
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
catch {
|
|
520
|
-
// memoryWriter failed — degrade gracefully
|
|
521
|
-
logEvent("memory_writeback_error", {
|
|
522
|
-
loopId: this.config.loopId,
|
|
523
|
-
stopReason,
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
}
|
|
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))));
|
|
527
429
|
}
|
|
528
430
|
this.emit("done", result);
|
|
529
431
|
return result;
|
|
530
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
|
+
}
|
|
531
482
|
/** Stop the loop gracefully. Safe to call from any thread/timer. */
|
|
532
483
|
stop() {
|
|
533
|
-
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();
|
|
534
489
|
this._status = RuntimeStatus.STOPPED;
|
|
535
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}`);
|
|
536
523
|
}
|
|
524
|
+
this._status = RuntimeStatus.RUNNING;
|
|
525
|
+
this.pauseTimestamp = 0;
|
|
526
|
+
this.emit("resume");
|
|
527
|
+
return this.launchDriver(true);
|
|
537
528
|
}
|
|
538
529
|
// ── Internal: compile request builder ─────────────────────────────────
|
|
539
530
|
buildCompileRequest(previousAgentOutput) {
|
|
@@ -546,7 +537,6 @@ export class LoopRuntime extends EventEmitter {
|
|
|
546
537
|
plan_source: this.config.planSource ?? null,
|
|
547
538
|
constraints_from_plan: this.config.constraintsFromPlan,
|
|
548
539
|
health_check_interval: this.config.healthCheckInterval,
|
|
549
|
-
vault_config: makeVaultConfig(),
|
|
550
540
|
});
|
|
551
541
|
if (this.timedOut && this.currentRound > 1) {
|
|
552
542
|
lcr.force_level = "l2";
|
|
@@ -575,11 +565,36 @@ export class LoopRuntime extends EventEmitter {
|
|
|
575
565
|
// v1.10: Checkpoint
|
|
576
566
|
compression_checkpoint: selfEval.compression_checkpoint ?? false,
|
|
577
567
|
checkpoint_label: selfEval.checkpoint_label ?? "",
|
|
568
|
+
// v1.16: Agent's declared next action
|
|
569
|
+
next_action: selfEval.next_action,
|
|
578
570
|
});
|
|
579
571
|
}
|
|
580
572
|
}
|
|
581
573
|
return lcr;
|
|
582
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
|
+
}
|
|
583
598
|
// ── Internal: heartbeat ──────────────────────────────────────────────
|
|
584
599
|
startHeartbeat() {
|
|
585
600
|
if (this.config.heartbeatIntervalMs <= 0)
|
|
@@ -623,28 +638,12 @@ export class LoopRuntime extends EventEmitter {
|
|
|
623
638
|
}
|
|
624
639
|
// Timeout
|
|
625
640
|
if (elapsed > this.config.roundTimeoutMs && !this.timedOut) {
|
|
626
|
-
this.
|
|
627
|
-
if (this.activeCtx) {
|
|
628
|
-
this.activeCtx.signal.aborted = true;
|
|
629
|
-
}
|
|
630
|
-
const timeoutInfo = {
|
|
631
|
-
round: this.currentRound,
|
|
632
|
-
elapsedMs: elapsed,
|
|
633
|
-
};
|
|
634
|
-
this.emit("timeout", timeoutInfo);
|
|
635
|
-
this.config.onTimeout?.(timeoutInfo);
|
|
641
|
+
this.triggerTimeout(elapsed);
|
|
636
642
|
}
|
|
637
643
|
// Stall — timeout + grace period elapsed, execute still hasn't returned
|
|
638
644
|
if (this.activeCtx !== null &&
|
|
639
645
|
elapsed > this.config.roundTimeoutMs + this.config.stallGraceMs) {
|
|
640
|
-
this.
|
|
641
|
-
const stallMsg = `Round ${this.currentRound} stalled after ${elapsed}ms`;
|
|
642
|
-
this.emit("stalled", {
|
|
643
|
-
reason: "round_timeout",
|
|
644
|
-
lastRound: this.currentRound,
|
|
645
|
-
elapsedMs: elapsed,
|
|
646
|
-
message: stallMsg,
|
|
647
|
-
});
|
|
646
|
+
this.triggerStall(elapsed);
|
|
648
647
|
}
|
|
649
648
|
}
|
|
650
649
|
catch {
|
|
@@ -660,7 +659,27 @@ export class LoopRuntime extends EventEmitter {
|
|
|
660
659
|
// ── Internal: signal handlers ────────────────────────────────────────
|
|
661
660
|
registerSignalHandlers() {
|
|
662
661
|
this.sigintHandler = () => {
|
|
663
|
-
|
|
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
|
+
}
|
|
664
683
|
};
|
|
665
684
|
this.sigtermHandler = () => {
|
|
666
685
|
this.stop();
|