loopforge 1.7.0 → 1.8.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 +64 -3
- package/dist/engine.d.ts +14 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +102 -1
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +23 -0
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +185 -19
- package/dist/loop-compiler.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +6 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +15 -3
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +199 -5
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +32 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +29 -10
- package/dist/mcp/tools.js.map +1 -1
- package/dist/memory-bridge.d.ts +47 -0
- package/dist/memory-bridge.d.ts.map +1 -0
- package/dist/memory-bridge.js +290 -0
- package/dist/memory-bridge.js.map +1 -0
- package/dist/policy.d.ts +57 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +36 -0
- package/dist/policy.js.map +1 -1
- package/dist/protocol.d.ts +84 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +5 -3
- package/dist/protocol.js.map +1 -1
- package/dist/runtime.d.ts +17 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +427 -171
- package/dist/runtime.js.map +1 -1
- package/dist/tests/generate-schema.test.js +2 -2
- package/dist/tests/loop-compiler.test.js +237 -1
- package/dist/tests/loop-compiler.test.js.map +1 -1
- package/dist/tests/mcp.test.js +95 -95
- package/dist/tests/mcp.test.js.map +1 -1
- package/dist/tests/memory-bridge.test.d.ts +8 -0
- package/dist/tests/memory-bridge.test.d.ts.map +1 -0
- package/dist/tests/memory-bridge.test.js +384 -0
- package/dist/tests/memory-bridge.test.js.map +1 -0
- package/dist/tests/protocol.test.js +12 -1
- package/dist/tests/protocol.test.js.map +1 -1
- package/loop_policy.json +23 -0
- package/package.json +1 -1
- package/skills/perception/SKILL.md +52 -1
- package/dist/adapter.d.ts +0 -17
- package/dist/adapter.d.ts.map +0 -1
- package/dist/adapter.js +0 -228
- package/dist/adapter.js.map +0 -1
- package/dist/tests/adapter.test.d.ts +0 -2
- package/dist/tests/adapter.test.d.ts.map +0 -1
package/dist/runtime.js
CHANGED
|
@@ -14,13 +14,82 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { EventEmitter } from "node:events";
|
|
16
16
|
import { extractSelfEvaluation, heuristicSelfEvaluation, createEngine, } from "./engine.js";
|
|
17
|
+
import { tokenize, jaccard } from "./loop-compiler.js";
|
|
17
18
|
import { Mode, RuntimeStatus, makeLoopCompileRequest, makeLoopRoundResult, makeSelfEvaluation, makeVaultConfig, } from "./protocol.js";
|
|
18
|
-
import { getPolicy } from "./policy.js";
|
|
19
|
+
import { getPolicy, resolveAllowedPhases } from "./policy.js";
|
|
19
20
|
import { verifySelfEvaluation } from "./verification-gate.js";
|
|
20
21
|
import { logEvent } from "./observability.js";
|
|
22
|
+
import { tryAutoConfigure } from "./memory-bridge.js";
|
|
21
23
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
24
|
// Helpers
|
|
23
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 quality trajectory patterns
|
|
47
|
+
const feedbackEntries = [];
|
|
48
|
+
const trajectory = result.qualityTrajectory;
|
|
49
|
+
if (trajectory.length >= 3) {
|
|
50
|
+
// Detect improving trend
|
|
51
|
+
const firstHalf = trajectory.slice(0, Math.floor(trajectory.length / 2));
|
|
52
|
+
const secondHalf = trajectory.slice(Math.floor(trajectory.length / 2));
|
|
53
|
+
const firstAvg = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length;
|
|
54
|
+
const secondAvg = secondHalf.reduce((a, b) => a + b, 0) / secondHalf.length;
|
|
55
|
+
if (secondAvg - firstAvg > 1.0) {
|
|
56
|
+
feedbackEntries.push({
|
|
57
|
+
rule: `Quality trajectory improved from avg ${firstAvg.toFixed(1)} to ${secondAvg.toFixed(1)} 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
|
|
64
|
+
if (trajectory.length >= 3) {
|
|
65
|
+
const recent = trajectory.slice(-3);
|
|
66
|
+
if (recent.every((v, i) => i === 0 || recent[i - 1] >= v)) {
|
|
67
|
+
feedbackEntries.push({
|
|
68
|
+
rule: `Quality flatlined at [${recent.join(", ")}] in the last 3 rounds`,
|
|
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 flatline",
|
|
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
|
+
qualityTrajectory: result.qualityTrajectory,
|
|
85
|
+
projectEntry: {
|
|
86
|
+
...projectEntry,
|
|
87
|
+
keyDiscoveries: projectEntry.keyDiscoveries.slice(0, wp.max_discoveries_in_project),
|
|
88
|
+
},
|
|
89
|
+
feedbackEntries: feedbackEntries.slice(0, wp.max_feedback_entries),
|
|
90
|
+
referenceEntry,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
24
93
|
function autoLoopId(task) {
|
|
25
94
|
let slug = task.toLowerCase().trim().slice(0, 60);
|
|
26
95
|
slug = slug.replace(/[^a-z0-9\s-]/g, "");
|
|
@@ -30,7 +99,7 @@ function autoLoopId(task) {
|
|
|
30
99
|
function resolveConfig(raw) {
|
|
31
100
|
const policy = getPolicy();
|
|
32
101
|
const rtPolicy = policy.runtime;
|
|
33
|
-
|
|
102
|
+
const config = {
|
|
34
103
|
task: raw.task,
|
|
35
104
|
execute: raw.execute,
|
|
36
105
|
loopId: raw.loopId || autoLoopId(raw.task),
|
|
@@ -50,7 +119,18 @@ function resolveConfig(raw) {
|
|
|
50
119
|
onHeartbeat: raw.onHeartbeat,
|
|
51
120
|
onTimeout: raw.onTimeout,
|
|
52
121
|
onHealthWarning: raw.onHealthWarning,
|
|
122
|
+
memoryProvider: raw.memoryProvider,
|
|
123
|
+
memoryWriter: raw.memoryWriter,
|
|
53
124
|
};
|
|
125
|
+
// Auto-detect claude-mem if no explicit provider was given
|
|
126
|
+
if (!config.memoryProvider && !config.memoryWriter) {
|
|
127
|
+
const auto = tryAutoConfigure();
|
|
128
|
+
if (auto.memoryProvider)
|
|
129
|
+
config.memoryProvider = auto.memoryProvider;
|
|
130
|
+
if (auto.memoryWriter)
|
|
131
|
+
config.memoryWriter = auto.memoryWriter;
|
|
132
|
+
}
|
|
133
|
+
return config;
|
|
54
134
|
}
|
|
55
135
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
56
136
|
// LoopRuntime
|
|
@@ -67,6 +147,13 @@ export class LoopRuntime extends EventEmitter {
|
|
|
67
147
|
// Verification gate state (v1.6 unified — was MCP-only, now also in runtime)
|
|
68
148
|
lastSelfEval = null;
|
|
69
149
|
pendingVerificationFlags = [];
|
|
150
|
+
// Memory integration state (v1.7)
|
|
151
|
+
injectionCount = 0;
|
|
152
|
+
lastInjectionRound = 0;
|
|
153
|
+
injectedContexts = [];
|
|
154
|
+
phase2Triggered = false;
|
|
155
|
+
phase3Triggered = false;
|
|
156
|
+
pendingExternalContext = "";
|
|
70
157
|
// Shared mutable state between the main loop and the heartbeat tick
|
|
71
158
|
roundStartTime = 0;
|
|
72
159
|
lastProgressTime = 0;
|
|
@@ -86,6 +173,95 @@ export class LoopRuntime extends EventEmitter {
|
|
|
86
173
|
getQualityTrajectory() {
|
|
87
174
|
return [...this.qualityTrajectory];
|
|
88
175
|
}
|
|
176
|
+
// ── Memory Integration (v1.7) ──────────────────────────────────────────
|
|
177
|
+
/** Derive the current progress estimate from the last self evaluation.
|
|
178
|
+
* Returns -1 if no progress data is available. */
|
|
179
|
+
getCurrentProgress() {
|
|
180
|
+
if (!this.lastSelfEval?.execution_evidence)
|
|
181
|
+
return -1;
|
|
182
|
+
const pe = this.lastSelfEval.execution_evidence.progress_estimate;
|
|
183
|
+
return typeof pe === "number" ? pe : -1;
|
|
184
|
+
}
|
|
185
|
+
/** Determine whether memory should be injected this round based on
|
|
186
|
+
* tier-based allowed phases, progress thresholds, and round spacing. */
|
|
187
|
+
shouldInjectMemory() {
|
|
188
|
+
const policy = getPolicy();
|
|
189
|
+
const mi = policy.memory_injection;
|
|
190
|
+
if (!mi.enabled)
|
|
191
|
+
return false;
|
|
192
|
+
if (!this.config.memoryProvider)
|
|
193
|
+
return false;
|
|
194
|
+
if (this.lastInjectionRound > 0 &&
|
|
195
|
+
this.currentRound - this.lastInjectionRound < mi.min_rounds_between_injections) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
// Resolve which phases are allowed for this loop's maxRounds
|
|
199
|
+
const allowedPhases = new Set(resolveAllowedPhases(this.config.maxRounds, mi.round_tiers));
|
|
200
|
+
const maxInjections = allowedPhases.size;
|
|
201
|
+
if (this.injectionCount >= maxInjections)
|
|
202
|
+
return false;
|
|
203
|
+
const progress = this.getCurrentProgress();
|
|
204
|
+
// Phase 1: round 1, always check if allowed
|
|
205
|
+
if (this.currentRound === 1 && this.injectionCount === 0 && allowedPhases.has(1)) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
// Phase 2: progress threshold (only if allowed by tier)
|
|
209
|
+
if (allowedPhases.has(2) &&
|
|
210
|
+
!this.phase2Triggered &&
|
|
211
|
+
progress >= mi.phase_thresholds.phase2.threshold) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
// Phase 3: progress threshold (only if allowed by tier)
|
|
215
|
+
if (allowedPhases.has(3) &&
|
|
216
|
+
!this.phase3Triggered &&
|
|
217
|
+
progress >= mi.phase_thresholds.phase3.threshold) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
/** Build the accumulated context for constructing a targeted memory query. */
|
|
223
|
+
buildAccumulatedContext() {
|
|
224
|
+
const recurringIssues = [];
|
|
225
|
+
const failedPatterns = [];
|
|
226
|
+
const keyLessons = [];
|
|
227
|
+
const remainingCriteria = [];
|
|
228
|
+
if (this.lastSelfEval) {
|
|
229
|
+
// Recurring issues: violations that have appeared before
|
|
230
|
+
if (this.lastSelfEval.constraint_violations.length) {
|
|
231
|
+
recurringIssues.push(...this.lastSelfEval.constraint_violations);
|
|
232
|
+
}
|
|
233
|
+
// Remaining criteria
|
|
234
|
+
if (this.lastSelfEval.execution_evidence?.success_criteria_remaining?.length) {
|
|
235
|
+
remainingCriteria.push(...this.lastSelfEval.execution_evidence.success_criteria_remaining);
|
|
236
|
+
}
|
|
237
|
+
// Key lessons from emerged subtasks
|
|
238
|
+
if (this.lastSelfEval.emerged_subtasks?.length) {
|
|
239
|
+
keyLessons.push(...this.lastSelfEval.emerged_subtasks);
|
|
240
|
+
}
|
|
241
|
+
// Wrong assumptions as negative lessons
|
|
242
|
+
if (this.lastSelfEval.wrong_assumptions?.length) {
|
|
243
|
+
keyLessons.push(...this.lastSelfEval.wrong_assumptions.map((a) => `Wrong: ${a}`));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return { recurringIssues, failedPatterns, keyLessons, remainingCriteria };
|
|
247
|
+
}
|
|
248
|
+
/** Deduplicate external context against previously injected contexts.
|
|
249
|
+
* Returns empty string if the new context is too similar to any prior. */
|
|
250
|
+
dedupAndStoreContext(newContext) {
|
|
251
|
+
if (!newContext.trim())
|
|
252
|
+
return "";
|
|
253
|
+
const policy = getPolicy();
|
|
254
|
+
const threshold = policy.memory_injection.dedup_threshold;
|
|
255
|
+
const newTokens = tokenize(newContext);
|
|
256
|
+
for (const old of this.injectedContexts) {
|
|
257
|
+
const oldTokens = tokenize(old);
|
|
258
|
+
if (jaccard(newTokens, oldTokens) > threshold) {
|
|
259
|
+
return ""; // Too similar — skip
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
this.injectedContexts.push(newContext);
|
|
263
|
+
return newContext;
|
|
264
|
+
}
|
|
89
265
|
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
90
266
|
/** Start the loop. Returns when the loop terminates (task complete,
|
|
91
267
|
* circuit breaker, max rounds, stalled, or manual stop). */
|
|
@@ -100,187 +276,241 @@ export class LoopRuntime extends EventEmitter {
|
|
|
100
276
|
let stopReason = "max_rounds";
|
|
101
277
|
let previousAgentOutput = null;
|
|
102
278
|
let consecutiveErrors = 0;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
// ── Compile ──────────────────────────────────────────────────
|
|
109
|
-
const lcr = this.buildCompileRequest(previousAgentOutput);
|
|
110
|
-
const compileResult = this.engine.invokeLoopCompile({
|
|
111
|
-
task: this.config.task,
|
|
112
|
-
mode: Mode.LOOP_COMPILE,
|
|
113
|
-
vault_config: makeVaultConfig(),
|
|
114
|
-
feedback: null,
|
|
115
|
-
skill_name: null,
|
|
116
|
-
task_id: null,
|
|
117
|
-
loop_id: lcr.loop_id,
|
|
118
|
-
round: lcr.round,
|
|
119
|
-
goal_id: lcr.goal_id,
|
|
120
|
-
domain: lcr.domain ?? "",
|
|
121
|
-
plan_source: lcr.plan_source ?? null,
|
|
122
|
-
constraints_from_plan: lcr.constraints_from_plan ?? [],
|
|
123
|
-
health_check_interval: lcr.health_check_interval,
|
|
124
|
-
last_round_result: (lcr.last_round_result ?? undefined),
|
|
125
|
-
verification_flags: this.pendingVerificationFlags,
|
|
126
|
-
});
|
|
127
|
-
if (!compileResult.response?.prompt) {
|
|
128
|
-
stopReason = "stalled";
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
const prompt = compileResult.response.prompt;
|
|
132
|
-
const analysis = compileResult.response.analysis;
|
|
133
|
-
const level = analysis?.rationale?.includes("l0")
|
|
134
|
-
? "l0"
|
|
135
|
-
: analysis?.rationale?.includes("l1")
|
|
136
|
-
? "l1"
|
|
137
|
-
: "l2";
|
|
138
|
-
const technique = analysis?.technique ?? "unknown";
|
|
139
|
-
// ── Emit round:start ──────────────────────────────────────────
|
|
140
|
-
const startInfo = { round: this.currentRound, level, technique, prompt };
|
|
141
|
-
this.emit("round:start", startInfo);
|
|
142
|
-
this.config.onRoundStart?.(startInfo);
|
|
143
|
-
// ── Execute ───────────────────────────────────────────────────
|
|
144
|
-
this.roundStartTime = Date.now();
|
|
145
|
-
this.lastProgressTime = this.roundStartTime;
|
|
146
|
-
this.timedOut = false;
|
|
147
|
-
const ctx = {
|
|
148
|
-
round: this.currentRound,
|
|
149
|
-
signal: { aborted: false },
|
|
150
|
-
reportProgress: (message) => {
|
|
151
|
-
this.lastProgressTime = Date.now();
|
|
152
|
-
void message; // consumed by heartbeat via lastProgressTime
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
this.activeCtx = ctx;
|
|
156
|
-
let agentOutput;
|
|
157
|
-
try {
|
|
158
|
-
agentOutput = await this.config.execute(prompt, ctx);
|
|
159
|
-
consecutiveErrors = 0;
|
|
160
|
-
}
|
|
161
|
-
catch (err) {
|
|
162
|
-
consecutiveErrors++;
|
|
163
|
-
const dur = Date.now() - this.roundStartTime;
|
|
164
|
-
this.activeCtx = null;
|
|
165
|
-
// Emit round:complete with error info
|
|
166
|
-
const errInfo = {
|
|
167
|
-
round: this.currentRound,
|
|
168
|
-
quality: 0,
|
|
169
|
-
selfEval: makeSelfEvaluation({
|
|
170
|
-
success: false,
|
|
171
|
-
output_summary: `execute threw: ${String(err).slice(0, 200)}`,
|
|
172
|
-
constraint_violations: ["execute() threw an exception"],
|
|
173
|
-
should_continue: true,
|
|
174
|
-
}),
|
|
175
|
-
durationMs: dur,
|
|
176
|
-
};
|
|
177
|
-
this.emit("round:complete", errInfo);
|
|
178
|
-
this.config.onRoundComplete?.(errInfo);
|
|
179
|
-
this.qualityTrajectory.push(0);
|
|
180
|
-
if (consecutiveErrors >= this.config.maxConsecutiveErrors) {
|
|
181
|
-
stopReason = "executor_failure";
|
|
279
|
+
try {
|
|
280
|
+
for (this.currentRound = 1; this.currentRound <= this.config.maxRounds; this.currentRound++) {
|
|
281
|
+
if (this._status !== RuntimeStatus.RUNNING) {
|
|
282
|
+
stopReason = this._status === RuntimeStatus.STALLED ? "stalled" : "stopped";
|
|
182
283
|
break;
|
|
183
284
|
}
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
285
|
+
// ── v1.7: Memory Injection (before compile, only at L2 phases) ──
|
|
286
|
+
this.pendingExternalContext = "";
|
|
287
|
+
if (this.shouldInjectMemory()) {
|
|
288
|
+
const progress = this.getCurrentProgress();
|
|
289
|
+
let phase;
|
|
290
|
+
if (this.currentRound === 1) {
|
|
291
|
+
phase = 1;
|
|
292
|
+
}
|
|
293
|
+
else if (!this.phase2Triggered && progress >= getPolicy().memory_injection.phase_thresholds.phase2.threshold) {
|
|
294
|
+
phase = 2;
|
|
295
|
+
this.phase2Triggered = true;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
phase = 3;
|
|
299
|
+
this.phase3Triggered = true;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const ctx = {
|
|
303
|
+
loopId: this.config.loopId,
|
|
304
|
+
round: this.currentRound,
|
|
305
|
+
task: this.config.task,
|
|
306
|
+
domain: this.config.domain,
|
|
307
|
+
phase,
|
|
308
|
+
progressEstimate: progress,
|
|
309
|
+
accumulatedContext: this.buildAccumulatedContext(),
|
|
310
|
+
};
|
|
311
|
+
const rawContext = await this.config.memoryProvider(ctx);
|
|
312
|
+
this.pendingExternalContext = this.dedupAndStoreContext(rawContext);
|
|
313
|
+
if (this.pendingExternalContext) {
|
|
314
|
+
this.injectionCount++;
|
|
315
|
+
this.lastInjectionRound = this.currentRound;
|
|
316
|
+
logEvent("memory_injected", {
|
|
317
|
+
loopId: this.config.loopId,
|
|
318
|
+
round: this.currentRound,
|
|
319
|
+
phase,
|
|
320
|
+
injectionCount: this.injectionCount,
|
|
321
|
+
contextLength: this.pendingExternalContext.length,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// memoryProvider failed — degrade gracefully, continue without context
|
|
327
|
+
this.pendingExternalContext = "";
|
|
328
|
+
logEvent("memory_provider_error", {
|
|
329
|
+
loopId: this.config.loopId,
|
|
330
|
+
round: this.currentRound,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// ── Compile ──────────────────────────────────────────────────
|
|
335
|
+
const lcr = this.buildCompileRequest(previousAgentOutput);
|
|
336
|
+
const compileResult = this.engine.invokeLoopCompile({
|
|
337
|
+
task: this.config.task,
|
|
338
|
+
mode: Mode.LOOP_COMPILE,
|
|
339
|
+
vault_config: makeVaultConfig(),
|
|
340
|
+
feedback: null,
|
|
341
|
+
skill_name: null,
|
|
342
|
+
task_id: null,
|
|
343
|
+
loop_id: lcr.loop_id,
|
|
344
|
+
round: lcr.round,
|
|
345
|
+
goal_id: lcr.goal_id,
|
|
346
|
+
domain: lcr.domain ?? "",
|
|
347
|
+
plan_source: lcr.plan_source ?? null,
|
|
348
|
+
constraints_from_plan: lcr.constraints_from_plan ?? [],
|
|
349
|
+
health_check_interval: lcr.health_check_interval,
|
|
350
|
+
last_round_result: (lcr.last_round_result ?? undefined),
|
|
351
|
+
verification_flags: this.pendingVerificationFlags,
|
|
352
|
+
external_context: this.pendingExternalContext || undefined,
|
|
210
353
|
});
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
354
|
+
if (!compileResult.response?.prompt) {
|
|
355
|
+
stopReason = "stalled";
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
const prompt = compileResult.response.prompt;
|
|
359
|
+
const analysis = compileResult.response.analysis;
|
|
360
|
+
const level = analysis?.rationale?.includes("l0")
|
|
361
|
+
? "l0"
|
|
362
|
+
: analysis?.rationale?.includes("l1")
|
|
363
|
+
? "l1"
|
|
364
|
+
: "l2";
|
|
365
|
+
const technique = analysis?.technique ?? "unknown";
|
|
366
|
+
// ── Emit round:start ──────────────────────────────────────────
|
|
367
|
+
const startInfo = { round: this.currentRound, level, technique, prompt };
|
|
368
|
+
this.emit("round:start", startInfo);
|
|
369
|
+
this.config.onRoundStart?.(startInfo);
|
|
370
|
+
// ── Execute ───────────────────────────────────────────────────
|
|
371
|
+
this.roundStartTime = Date.now();
|
|
372
|
+
this.lastProgressTime = this.roundStartTime;
|
|
373
|
+
this.timedOut = false;
|
|
374
|
+
const ctx = {
|
|
375
|
+
round: this.currentRound,
|
|
376
|
+
signal: { aborted: false },
|
|
377
|
+
reportProgress: (message) => {
|
|
378
|
+
this.lastProgressTime = Date.now();
|
|
379
|
+
void message; // consumed by heartbeat via lastProgressTime
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
this.activeCtx = ctx;
|
|
383
|
+
let agentOutput;
|
|
384
|
+
try {
|
|
385
|
+
agentOutput = await this.config.execute(prompt, ctx);
|
|
386
|
+
consecutiveErrors = 0;
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
consecutiveErrors++;
|
|
390
|
+
const dur = Date.now() - this.roundStartTime;
|
|
391
|
+
this.activeCtx = null;
|
|
392
|
+
// Emit round:complete with error info
|
|
393
|
+
const errInfo = {
|
|
217
394
|
round: this.currentRound,
|
|
218
|
-
|
|
395
|
+
quality: 0,
|
|
396
|
+
selfEval: makeSelfEvaluation({
|
|
397
|
+
success: false,
|
|
398
|
+
output_summary: `execute threw: ${String(err).slice(0, 200)}`,
|
|
399
|
+
constraint_violations: ["execute() threw an exception"],
|
|
400
|
+
should_continue: true,
|
|
401
|
+
}),
|
|
402
|
+
durationMs: dur,
|
|
403
|
+
};
|
|
404
|
+
this.emit("round:complete", errInfo);
|
|
405
|
+
this.config.onRoundComplete?.(errInfo);
|
|
406
|
+
this.qualityTrajectory.push(0);
|
|
407
|
+
if (consecutiveErrors >= this.config.maxConsecutiveErrors) {
|
|
408
|
+
stopReason = "executor_failure";
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
// Record as a failed round so the next round's compilation
|
|
412
|
+
// sees the failure context (forces L1/L2)
|
|
413
|
+
previousAgentOutput = null;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const durationMs = Date.now() - this.roundStartTime;
|
|
417
|
+
this.activeCtx = null;
|
|
418
|
+
// ── Extract self-eval ─────────────────────────────────────────
|
|
419
|
+
let selfEval = extractSelfEvaluation(agentOutput);
|
|
420
|
+
let extractionSucceeded = selfEval !== null;
|
|
421
|
+
if (selfEval === null) {
|
|
422
|
+
selfEval = heuristicSelfEvaluation(agentOutput);
|
|
423
|
+
extractionSucceeded = false;
|
|
424
|
+
}
|
|
425
|
+
// ── Auto-feedback ─────────────────────────────────────────────
|
|
426
|
+
if (selfEval !== null) {
|
|
427
|
+
this.engine.autoFeedback(selfEval, this.config.loopId, this.currentRound, this.config.task);
|
|
428
|
+
}
|
|
429
|
+
// ── Verification gate (v1.6 unified) ──────────────────────────
|
|
430
|
+
// Run cross-round consistency checks between self-eval and lineage.
|
|
431
|
+
// Flags are injected into the NEXT round's prompt via the engine.
|
|
432
|
+
this.pendingVerificationFlags = [];
|
|
433
|
+
let gateContradicted = false;
|
|
434
|
+
if (selfEval !== null) {
|
|
435
|
+
const vaultEntries = this.engine.getBackend().queryEntries({
|
|
436
|
+
prefix: `loop:${this.config.loopId}:r`,
|
|
219
437
|
});
|
|
438
|
+
const verifyResult = verifySelfEvaluation(selfEval, this.currentRound, vaultEntries, this.lastSelfEval);
|
|
439
|
+
this.pendingVerificationFlags = verifyResult.flags;
|
|
440
|
+
if (verifyResult.verdict === "contradicted") {
|
|
441
|
+
gateContradicted = true;
|
|
442
|
+
logEvent("gate_contradicted", {
|
|
443
|
+
loopId: this.config.loopId,
|
|
444
|
+
round: this.currentRound,
|
|
445
|
+
flags: verifyResult.flags.map((f) => f.check),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
this.lastSelfEval = selfEval;
|
|
220
449
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
450
|
+
// Compute quality score
|
|
451
|
+
let quality = 0;
|
|
452
|
+
if (selfEval) {
|
|
453
|
+
if (selfEval.success && selfEval.constraint_violations.length === 0) {
|
|
454
|
+
quality = 5;
|
|
455
|
+
}
|
|
456
|
+
else if (selfEval.success) {
|
|
457
|
+
quality = 3;
|
|
458
|
+
}
|
|
459
|
+
else if (selfEval.constraint_violations.length > 0) {
|
|
460
|
+
quality = 2;
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
quality = 1;
|
|
464
|
+
}
|
|
228
465
|
}
|
|
229
|
-
|
|
230
|
-
|
|
466
|
+
// Contradicted gate verdict: skip quality trend (match MCP path behavior)
|
|
467
|
+
if (!gateContradicted) {
|
|
468
|
+
this.qualityTrajectory.push(quality);
|
|
231
469
|
}
|
|
232
|
-
|
|
233
|
-
|
|
470
|
+
// ── Emit round:complete ───────────────────────────────────────
|
|
471
|
+
const completeInfo = {
|
|
472
|
+
round: this.currentRound,
|
|
473
|
+
quality,
|
|
474
|
+
selfEval,
|
|
475
|
+
durationMs,
|
|
476
|
+
};
|
|
477
|
+
this.emit("round:complete", completeInfo);
|
|
478
|
+
this.config.onRoundComplete?.(completeInfo);
|
|
479
|
+
logEvent("round_complete", {
|
|
480
|
+
loopId: this.config.loopId ?? "unknown",
|
|
481
|
+
round: this.currentRound,
|
|
482
|
+
quality,
|
|
483
|
+
durationMs,
|
|
484
|
+
technique: "loop_runtime",
|
|
485
|
+
});
|
|
486
|
+
// ── Check stop conditions ─────────────────────────────────────
|
|
487
|
+
// Extraction check FIRST — heuristic results are low-confidence
|
|
488
|
+
if (!extractionSucceeded) {
|
|
489
|
+
stopReason = "stalled"; // extraction_failure mapped to stalled
|
|
490
|
+
break;
|
|
234
491
|
}
|
|
235
|
-
|
|
236
|
-
|
|
492
|
+
if (selfEval && !selfEval.should_continue) {
|
|
493
|
+
stopReason = "task_complete";
|
|
494
|
+
break;
|
|
237
495
|
}
|
|
496
|
+
if (this.engine.shouldBreak()) {
|
|
497
|
+
stopReason = "circuit_breaker";
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
if (this.timedOut) {
|
|
501
|
+
// Next round will be forced L2 via the compile request
|
|
502
|
+
this.timedOut = false;
|
|
503
|
+
}
|
|
504
|
+
previousAgentOutput = agentOutput;
|
|
238
505
|
}
|
|
239
|
-
// Contradicted gate verdict: skip quality trend (match MCP path behavior)
|
|
240
|
-
if (!gateContradicted) {
|
|
241
|
-
this.qualityTrajectory.push(quality);
|
|
242
|
-
}
|
|
243
|
-
// ── Emit round:complete ───────────────────────────────────────
|
|
244
|
-
const completeInfo = {
|
|
245
|
-
round: this.currentRound,
|
|
246
|
-
quality,
|
|
247
|
-
selfEval,
|
|
248
|
-
durationMs,
|
|
249
|
-
};
|
|
250
|
-
this.emit("round:complete", completeInfo);
|
|
251
|
-
this.config.onRoundComplete?.(completeInfo);
|
|
252
|
-
logEvent("round_complete", {
|
|
253
|
-
loopId: this.config.loopId ?? "unknown",
|
|
254
|
-
round: this.currentRound,
|
|
255
|
-
quality,
|
|
256
|
-
durationMs,
|
|
257
|
-
technique: "loop_runtime",
|
|
258
|
-
});
|
|
259
|
-
// ── Check stop conditions ─────────────────────────────────────
|
|
260
|
-
// Extraction check FIRST — heuristic results are low-confidence
|
|
261
|
-
if (!extractionSucceeded) {
|
|
262
|
-
stopReason = "stalled"; // extraction_failure mapped to stalled
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
if (selfEval && !selfEval.should_continue) {
|
|
266
|
-
stopReason = "task_complete";
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
269
|
-
if (this.engine.shouldBreak()) {
|
|
270
|
-
stopReason = "circuit_breaker";
|
|
271
|
-
break;
|
|
272
|
-
}
|
|
273
|
-
if (this.timedOut) {
|
|
274
|
-
// Next round will be forced L2 via the compile request
|
|
275
|
-
this.timedOut = false;
|
|
276
|
-
}
|
|
277
|
-
previousAgentOutput = agentOutput;
|
|
278
506
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
this._status
|
|
507
|
+
finally {
|
|
508
|
+
// ── Cleanup — always runs even if the loop body throws ──────────
|
|
509
|
+
this.stopHeartbeat();
|
|
510
|
+
this.unregisterSignalHandlers();
|
|
511
|
+
if (this._status === RuntimeStatus.RUNNING) {
|
|
512
|
+
this._status = RuntimeStatus.STOPPED;
|
|
513
|
+
}
|
|
284
514
|
}
|
|
285
515
|
const result = {
|
|
286
516
|
success: stopReason === "task_complete",
|
|
@@ -290,6 +520,32 @@ export class LoopRuntime extends EventEmitter {
|
|
|
290
520
|
: this.currentRound,
|
|
291
521
|
qualityTrajectory: [...this.qualityTrajectory],
|
|
292
522
|
};
|
|
523
|
+
// ── v1.7: Memory Writeback ───────────────────────────────────────
|
|
524
|
+
const wp = getPolicy().memory_writeback;
|
|
525
|
+
if (wp.enabled && this.config.memoryWriter) {
|
|
526
|
+
const shouldWrite = wp.write_on_outcomes.includes(stopReason) ||
|
|
527
|
+
wp.write_on_outcomes.length === 0;
|
|
528
|
+
if (shouldWrite && result.roundsCompleted > 0) {
|
|
529
|
+
try {
|
|
530
|
+
const payload = buildWritebackPayload(this.config.loopId, this.config.task, stopReason, result);
|
|
531
|
+
if (payload) {
|
|
532
|
+
await this.config.memoryWriter(payload);
|
|
533
|
+
logEvent("memory_writeback", {
|
|
534
|
+
loopId: this.config.loopId,
|
|
535
|
+
stopReason,
|
|
536
|
+
feedbackCount: payload.feedbackEntries.length,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
// memoryWriter failed — degrade gracefully
|
|
542
|
+
logEvent("memory_writeback_error", {
|
|
543
|
+
loopId: this.config.loopId,
|
|
544
|
+
stopReason,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
293
549
|
this.emit("done", result);
|
|
294
550
|
return result;
|
|
295
551
|
}
|