loopforge 1.3.1 → 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/backends/fs.d.ts +5 -0
- package/dist/backends/fs.d.ts.map +1 -1
- package/dist/backends/fs.js +66 -16
- package/dist/backends/fs.js.map +1 -1
- package/dist/builder.d.ts +0 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +14 -25
- package/dist/builder.js.map +1 -1
- package/dist/engine.d.ts +28 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +303 -105
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +9 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -10
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +28 -2
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +718 -38
- 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 +24 -4
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +303 -34
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +165 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +144 -15
- 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/observability.d.ts +23 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +29 -0
- package/dist/observability.js.map +1 -0
- package/dist/policy.d.ts +72 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +44 -0
- package/dist/policy.js.map +1 -1
- package/dist/protocol.d.ts +197 -7
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +48 -5
- package/dist/protocol.js.map +1 -1
- package/dist/runtime.d.ts +19 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +446 -142
- package/dist/runtime.js.map +1 -1
- package/dist/tests/builder.test.js +1 -25
- package/dist/tests/builder.test.js.map +1 -1
- package/dist/tests/engine.test.js +213 -38
- package/dist/tests/engine.test.js.map +1 -1
- package/dist/tests/generate-schema.test.js +4 -4
- package/dist/tests/generate-schema.test.js.map +1 -1
- package/dist/tests/loop-compiler.test.js +427 -2
- package/dist/tests/loop-compiler.test.js.map +1 -1
- package/dist/tests/mcp.test.js +166 -79
- 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 +14 -5
- package/dist/tests/protocol.test.js.map +1 -1
- package/dist/tests/verification-gate.test.d.ts +2 -0
- package/dist/tests/verification-gate.test.d.ts.map +1 -0
- package/dist/tests/verification-gate.test.js +407 -0
- package/dist/tests/verification-gate.test.js.map +1 -0
- package/dist/verification-gate.d.ts +26 -0
- package/dist/verification-gate.d.ts.map +1 -0
- package/dist/verification-gate.js +232 -0
- package/dist/verification-gate.js.map +1 -0
- package/loop_policy.json +31 -0
- package/package.json +1 -2
- package/skills/perception/SKILL.md +147 -55
- 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/cli.d.ts +0 -7
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -458
- package/dist/cli.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/tests/adapter.test.js +0 -104
- package/dist/tests/adapter.test.js.map +0 -1
package/dist/loop-compiler.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import { readFileSync } from "node:fs";
|
|
14
14
|
import { getPolicy } from "./policy.js";
|
|
15
|
-
import { makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeRollingSummary, makeTaskAlignment, } from "./protocol.js";
|
|
15
|
+
import { AgentStatus, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeRollingSummary, makeTaskAlignment, } from "./protocol.js";
|
|
16
16
|
import { routeTechniqueAdaptive } from "./builder.js";
|
|
17
17
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
18
18
|
// Repair cue detection
|
|
@@ -34,7 +34,7 @@ function detectsRepairSignal(request) {
|
|
|
34
34
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
35
35
|
// Tokenization helpers
|
|
36
36
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
-
function tokenize(text) {
|
|
37
|
+
export function tokenize(text) {
|
|
38
38
|
const tokens = text.split(/\s+/);
|
|
39
39
|
const result = new Set();
|
|
40
40
|
for (let token of tokens) {
|
|
@@ -52,7 +52,7 @@ function tokenize(text) {
|
|
|
52
52
|
}
|
|
53
53
|
return result;
|
|
54
54
|
}
|
|
55
|
-
function jaccard(a, b) {
|
|
55
|
+
export function jaccard(a, b) {
|
|
56
56
|
if (a.size === 0 || b.size === 0)
|
|
57
57
|
return 0.0;
|
|
58
58
|
let intersection = 0;
|
|
@@ -64,6 +64,141 @@ function jaccard(a, b) {
|
|
|
64
64
|
return union > 0 ? intersection / union : 0.0;
|
|
65
65
|
}
|
|
66
66
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
+
// Delegation helpers (v1.9 — AgentTool mode lightweight utilities)
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
+
/** Filter relevant constraints for a sub-agent task using Jaccard token similarity.
|
|
70
|
+
* Returns constraints whose token overlap with the subTask exceeds the threshold.
|
|
71
|
+
* Default threshold 0.15 is intentionally lower than the 0.3/0.5 alignment thresholds
|
|
72
|
+
* — constraint filtering should err on the side of inclusion. */
|
|
73
|
+
export function filterConstraintsForSubTask(allConstraints, subTask, threshold = 0.15) {
|
|
74
|
+
if (!allConstraints.length || !subTask.trim())
|
|
75
|
+
return [];
|
|
76
|
+
const taskTokens = tokenize(subTask);
|
|
77
|
+
if (taskTokens.size === 0)
|
|
78
|
+
return [];
|
|
79
|
+
return allConstraints.filter((c) => {
|
|
80
|
+
const cTokens = tokenize(c);
|
|
81
|
+
return jaccard(taskTokens, cTokens) >= threshold;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** Format a self-contained delegation prompt for a sub-agent.
|
|
85
|
+
* Produces a prompt that stands alone — no references to parent conversation,
|
|
86
|
+
* no "based on above", no "continue from previous". This matches the AgentTool
|
|
87
|
+
* contract: "Workers can't see your conversation." */
|
|
88
|
+
export function formatDelegationPrompt(subTask, subAgentType, relevantConstraints, options) {
|
|
89
|
+
const lines = [];
|
|
90
|
+
// Header
|
|
91
|
+
lines.push("### Delegated Task");
|
|
92
|
+
lines.push("");
|
|
93
|
+
lines.push(subTask);
|
|
94
|
+
lines.push("");
|
|
95
|
+
// Context (optional — e.g. file paths, module names)
|
|
96
|
+
if (options?.context) {
|
|
97
|
+
lines.push("### Context");
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push(options.context);
|
|
100
|
+
lines.push("");
|
|
101
|
+
}
|
|
102
|
+
// Constraints (only for types that need them — Explore is read-only)
|
|
103
|
+
if (relevantConstraints.length && subAgentType !== "explore") {
|
|
104
|
+
lines.push("### Relevant Constraints");
|
|
105
|
+
for (const c of relevantConstraints)
|
|
106
|
+
lines.push(`- ${c}`);
|
|
107
|
+
lines.push("");
|
|
108
|
+
}
|
|
109
|
+
// Output format (for Explore and Plan agents)
|
|
110
|
+
if (subAgentType === "explore") {
|
|
111
|
+
if (options?.outputFormat) {
|
|
112
|
+
lines.push("### Output Format");
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push(options.outputFormat);
|
|
115
|
+
lines.push("");
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
lines.push("### Output Format");
|
|
119
|
+
lines.push("");
|
|
120
|
+
lines.push("Report findings as a structured list with file paths and line numbers.");
|
|
121
|
+
lines.push("");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (subAgentType === "plan") {
|
|
125
|
+
lines.push("### Instructions");
|
|
126
|
+
lines.push("");
|
|
127
|
+
lines.push("Design a concrete implementation plan. Output must include:");
|
|
128
|
+
lines.push("- Step-by-step approach");
|
|
129
|
+
lines.push("- Critical files for implementation (3-5 files with paths)");
|
|
130
|
+
lines.push("- Key design decisions and tradeoffs");
|
|
131
|
+
lines.push("");
|
|
132
|
+
}
|
|
133
|
+
if (subAgentType === "general-purpose") {
|
|
134
|
+
lines.push("### Instructions");
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push("Complete the task fully — don't gold-plate, but don't leave it half-done.");
|
|
137
|
+
lines.push("Report what was done, what files were changed, and any issues encountered.");
|
|
138
|
+
lines.push("");
|
|
139
|
+
}
|
|
140
|
+
if (!["explore", "general-purpose", "plan"].includes(subAgentType)) {
|
|
141
|
+
// Custom sub-agent type — generic template
|
|
142
|
+
lines.push("### Instructions");
|
|
143
|
+
lines.push("");
|
|
144
|
+
lines.push("Complete the task above and report results concisely.");
|
|
145
|
+
lines.push("");
|
|
146
|
+
}
|
|
147
|
+
return lines.join("\n");
|
|
148
|
+
}
|
|
149
|
+
/** Build a delegation history summary from vault context (v1.9 — multi-agent).
|
|
150
|
+
* Scans vault for delegation_journal entries and formats them as a table.
|
|
151
|
+
* Returns empty string if no delegation history exists. */
|
|
152
|
+
export function buildDelegationSummary(vaultContext) {
|
|
153
|
+
if (!vaultContext)
|
|
154
|
+
return "";
|
|
155
|
+
const results = vaultContext.results;
|
|
156
|
+
if (!results?.length)
|
|
157
|
+
return "";
|
|
158
|
+
const delegations = results.filter((r) => r?.task_type === "delegation_journal");
|
|
159
|
+
if (!delegations.length)
|
|
160
|
+
return "";
|
|
161
|
+
// Helper: escape pipe characters so they don't break the markdown table
|
|
162
|
+
const esc = (s) => s.replace(/\|/g, "\\|");
|
|
163
|
+
// Collect all delegation entries across rounds
|
|
164
|
+
const entries = [];
|
|
165
|
+
for (const d of delegations) {
|
|
166
|
+
const lineage = d.loop_lineage;
|
|
167
|
+
const round = typeof lineage?.round === "number" ? lineage.round : 0;
|
|
168
|
+
const dels = lineage?.delegations;
|
|
169
|
+
if (!dels?.length)
|
|
170
|
+
continue;
|
|
171
|
+
for (const e of dels) {
|
|
172
|
+
entries.push({
|
|
173
|
+
round,
|
|
174
|
+
agentId: typeof e.agentId === "string" ? e.agentId : (typeof e.index === "number" ? `worker-${e.index}` : "?"),
|
|
175
|
+
type: typeof e.subAgentType === "string" ? e.subAgentType : "?",
|
|
176
|
+
task: typeof e.subTask === "string" ? e.subTask : "?",
|
|
177
|
+
result: typeof e.resultSummary === "string" ? e.resultSummary : "?",
|
|
178
|
+
success: typeof e.success === "boolean" ? e.success : true,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!entries.length)
|
|
183
|
+
return "";
|
|
184
|
+
const lines = [
|
|
185
|
+
"",
|
|
186
|
+
"### Delegation History (Multi-Agent)",
|
|
187
|
+
"",
|
|
188
|
+
"| Round | Agent | Type | Task | Result | ✓/✗ |",
|
|
189
|
+
"|-------|-------|------|------|--------|-----|",
|
|
190
|
+
];
|
|
191
|
+
for (const e of entries) {
|
|
192
|
+
const status = e.success ? "✓" : "✗";
|
|
193
|
+
const shortAgent = e.agentId.length > 24 ? e.agentId.slice(0, 21) + "..." : e.agentId;
|
|
194
|
+
const shortTask = e.task.length > 50 ? e.task.slice(0, 47) + "..." : e.task;
|
|
195
|
+
const shortResult = e.result.length > 40 ? e.result.slice(0, 37) + "..." : e.result;
|
|
196
|
+
lines.push(`| R${e.round} | ${esc(shortAgent)} | ${e.type} | ${esc(shortTask)} | ${esc(shortResult)} | ${status} |`);
|
|
197
|
+
}
|
|
198
|
+
lines.push("");
|
|
199
|
+
return lines.join("\n");
|
|
200
|
+
}
|
|
201
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
67
202
|
// Goal identity
|
|
68
203
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
204
|
export function computeGoalTextHash(task) {
|
|
@@ -77,17 +212,6 @@ export function deriveGoalId(loopId, task, explicitGoalId = "") {
|
|
|
77
212
|
taskPrefix = taskPrefix.replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
78
213
|
return loopId ? `${loopId}:${taskPrefix}` : taskPrefix;
|
|
79
214
|
}
|
|
80
|
-
function makePreviousRound() {
|
|
81
|
-
return {
|
|
82
|
-
goal_id: "",
|
|
83
|
-
goal_text_hash: "",
|
|
84
|
-
quality_score: 0,
|
|
85
|
-
success: true,
|
|
86
|
-
task: "",
|
|
87
|
-
constraints_active: [],
|
|
88
|
-
prompt_text: "",
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
215
|
export function getPreviousRound(loopId, roundNum, vaultContext) {
|
|
92
216
|
if (vaultContext === null)
|
|
93
217
|
return null;
|
|
@@ -237,9 +361,140 @@ function computeConstraintRetirement(activeConstraints, loopId, currentRound, va
|
|
|
237
361
|
return { active: pruned, retired };
|
|
238
362
|
}
|
|
239
363
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
364
|
+
// P4: Progress evidence helpers
|
|
365
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
366
|
+
/** Extract execution evidence from a previous round in the vault.
|
|
367
|
+
* Used for progress gradient calculation and trend detection. */
|
|
368
|
+
function getPreviousRoundEvidence(loopId, roundNum, vaultContext) {
|
|
369
|
+
if (vaultContext === null)
|
|
370
|
+
return null;
|
|
371
|
+
const results = vaultContext.results || [];
|
|
372
|
+
for (const r of results) {
|
|
373
|
+
const lineage = (r.loop_lineage || r.lineage || {});
|
|
374
|
+
if (lineage.loop_id !== loopId)
|
|
375
|
+
continue;
|
|
376
|
+
if (lineage.round !== roundNum)
|
|
377
|
+
continue;
|
|
378
|
+
const evidence = (r.execution_evidence ?? lineage.execution_evidence);
|
|
379
|
+
if (evidence) {
|
|
380
|
+
return {
|
|
381
|
+
progress_estimate: evidence.progress_estimate ?? 0,
|
|
382
|
+
success_criteria_met: evidence.success_criteria_met ?? [],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
/** Count consecutive rounds (including current) where progress delta
|
|
390
|
+
* has been below the stall threshold. */
|
|
391
|
+
function countProgressStallRounds(loopId, currentRound, vaultContext, threshold) {
|
|
392
|
+
if (vaultContext === null || currentRound < 2)
|
|
393
|
+
return 0;
|
|
394
|
+
let count = 0;
|
|
395
|
+
for (let r = currentRound; r >= 2; r--) {
|
|
396
|
+
const curr = getPreviousRoundEvidence(loopId, r, vaultContext);
|
|
397
|
+
const prev = getPreviousRoundEvidence(loopId, r - 1, vaultContext);
|
|
398
|
+
if (curr && prev) {
|
|
399
|
+
const delta = curr.progress_estimate - prev.progress_estimate;
|
|
400
|
+
if (delta < threshold && curr.progress_estimate < 0.95) {
|
|
401
|
+
count++;
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return count;
|
|
412
|
+
}
|
|
413
|
+
/** Detect repeated failure patterns in sampled rounds.
|
|
414
|
+
* A failure pattern = 2+ consecutive rounds where:
|
|
415
|
+
* 1. quality_score < 3 (failed or weak)
|
|
416
|
+
* 2. same technique was used
|
|
417
|
+
* 3. task text is similar (Jaccard > 0.4)
|
|
418
|
+
*
|
|
419
|
+
* Returns human-readable descriptions for injection into the prompt. */
|
|
420
|
+
function detectFailurePatterns(sampled) {
|
|
421
|
+
if (sampled.length < 2)
|
|
422
|
+
return [];
|
|
423
|
+
const patterns = [];
|
|
424
|
+
let runStart = -1;
|
|
425
|
+
for (let i = 0; i < sampled.length; i++) {
|
|
426
|
+
const r = sampled[i];
|
|
427
|
+
const isFailed = r.quality_score < 3;
|
|
428
|
+
if (isFailed && runStart === -1) {
|
|
429
|
+
runStart = i;
|
|
430
|
+
}
|
|
431
|
+
else if (!isFailed && runStart !== -1) {
|
|
432
|
+
if (i - runStart >= 2) {
|
|
433
|
+
const pattern = classifyRun(sampled.slice(runStart, i));
|
|
434
|
+
if (pattern)
|
|
435
|
+
patterns.push(pattern);
|
|
436
|
+
}
|
|
437
|
+
runStart = -1;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Handle run at end of array
|
|
441
|
+
if (runStart !== -1 && sampled.length - runStart >= 2) {
|
|
442
|
+
const pattern = classifyRun(sampled.slice(runStart));
|
|
443
|
+
if (pattern)
|
|
444
|
+
patterns.push(pattern);
|
|
445
|
+
}
|
|
446
|
+
return patterns;
|
|
447
|
+
}
|
|
448
|
+
/** Classify a run of consecutive low-quality rounds as a failure pattern.
|
|
449
|
+
* Returns null if the rounds are too dissimilar to be the same pattern. */
|
|
450
|
+
function classifyRun(run) {
|
|
451
|
+
if (run.length < 2)
|
|
452
|
+
return null;
|
|
453
|
+
// Must all use the same technique
|
|
454
|
+
const technique = run[0].technique_used || "unknown";
|
|
455
|
+
if (!run.every((r) => (r.technique_used || "unknown") === technique)) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
// Task text must be similar across the run (pairwise Jaccard)
|
|
459
|
+
const tokensList = run.map((r) => tokenize(r.task.toLowerCase()));
|
|
460
|
+
for (let i = 0; i < tokensList.length - 1; i++) {
|
|
461
|
+
for (let j = i + 1; j < tokensList.length; j++) {
|
|
462
|
+
if (jaccard(tokensList[i], tokensList[j]) < 0.4)
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const rounds = `R${run[0].round}-R${run[run.length - 1].round}`;
|
|
467
|
+
const taskPreview = run[0].task.slice(0, 80);
|
|
468
|
+
const avgScore = (run.reduce((s, r) => s + r.quality_score, 0) / run.length).toFixed(1);
|
|
469
|
+
return (`technique '${technique}' on task '${taskPreview}' ` +
|
|
470
|
+
`failed ${run.length} consecutive rounds (${rounds}, avg score ${avgScore}) — ` +
|
|
471
|
+
`consider strategy change`);
|
|
472
|
+
}
|
|
473
|
+
/** Extract round numbers referenced in failure pattern descriptions.
|
|
474
|
+
* Used by buildRollingSummary to identify which rounds to demote. */
|
|
475
|
+
function extractFailureRoundNums(patterns) {
|
|
476
|
+
const nums = new Set();
|
|
477
|
+
for (const p of patterns) {
|
|
478
|
+
// Match "R2-R4" or "R5"
|
|
479
|
+
const rangeMatch = p.match(/R(\d+)-R(\d+)/);
|
|
480
|
+
if (rangeMatch) {
|
|
481
|
+
const start = parseInt(rangeMatch[1], 10);
|
|
482
|
+
const end = parseInt(rangeMatch[2], 10);
|
|
483
|
+
for (let r = start; r <= end; r++)
|
|
484
|
+
nums.add(r);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
const singleMatch = p.match(/R(\d+)/);
|
|
488
|
+
if (singleMatch)
|
|
489
|
+
nums.add(parseInt(singleMatch[1], 10));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return nums;
|
|
493
|
+
}
|
|
494
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
240
495
|
// Rolling Summary (v3.5)
|
|
241
496
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
242
|
-
function buildRollingSummary(loopId, currentRound, vaultContext) {
|
|
497
|
+
export function buildRollingSummary(loopId, currentRound, vaultContext) {
|
|
243
498
|
if (vaultContext === null)
|
|
244
499
|
return null;
|
|
245
500
|
const results = vaultContext.results || [];
|
|
@@ -310,7 +565,7 @@ function buildRollingSummary(loopId, currentRound, vaultContext) {
|
|
|
310
565
|
}
|
|
311
566
|
}
|
|
312
567
|
}
|
|
313
|
-
|
|
568
|
+
let recurringIssues = [...violationCounts.entries()]
|
|
314
569
|
.filter(([, count]) => count >= 2)
|
|
315
570
|
.sort((a, b) => b[1] - a[1])
|
|
316
571
|
.map(([v, count]) => `${v} (appeared in ${count} rounds)`);
|
|
@@ -321,6 +576,57 @@ function buildRollingSummary(loopId, currentRound, vaultContext) {
|
|
|
321
576
|
keyLessons.push(`[R${r.round}] ${String(r.output_summary).slice(0, 200)}`);
|
|
322
577
|
}
|
|
323
578
|
}
|
|
579
|
+
// v1.7: Failure lineage weighting — detect and demote failure patterns
|
|
580
|
+
const sampledRounds = sampled.map((r) => ({
|
|
581
|
+
round: r.round,
|
|
582
|
+
quality_score: r.quality_score,
|
|
583
|
+
task: r.task,
|
|
584
|
+
output_summary: r.output_summary,
|
|
585
|
+
constraint_violations: r.constraint_violations,
|
|
586
|
+
technique_used: r.technique_used,
|
|
587
|
+
}));
|
|
588
|
+
const failurePatterns = detectFailurePatterns(sampledRounds);
|
|
589
|
+
const failedRoundNums = extractFailureRoundNums(failurePatterns);
|
|
590
|
+
// Re-weight key_lessons: lessons from failure-pattern rounds go to end
|
|
591
|
+
if (failedRoundNums.size > 0 && keyLessons.length > 0) {
|
|
592
|
+
const promoted = [];
|
|
593
|
+
const demoted = [];
|
|
594
|
+
for (const kl of keyLessons) {
|
|
595
|
+
const rndMatch = kl.match(/^\[R(\d+)\]/);
|
|
596
|
+
if (rndMatch && failedRoundNums.has(parseInt(rndMatch[1], 10))) {
|
|
597
|
+
demoted.push(kl.replace(/^\[R\d+\]/, "$& [Consider alternatives]"));
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
promoted.push(kl);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
keyLessons.length = 0;
|
|
604
|
+
keyLessons.push(...promoted, ...demoted);
|
|
605
|
+
}
|
|
606
|
+
// Re-weight recurring_issues: mark issues that ONLY appear in failure-pattern rounds
|
|
607
|
+
if (failedRoundNums.size > 0 && recurringIssues.length > 0) {
|
|
608
|
+
// Build per-round violation sets
|
|
609
|
+
const failureViolations = new Set();
|
|
610
|
+
const successViolations = new Set();
|
|
611
|
+
for (const r of sampledRounds) {
|
|
612
|
+
const target = failedRoundNums.has(r.round) ? failureViolations : successViolations;
|
|
613
|
+
for (const v of r.constraint_violations) {
|
|
614
|
+
target.add(String(v).trim().toLowerCase());
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
recurringIssues.length = 0;
|
|
618
|
+
recurringIssues.push(...[...violationCounts.entries()]
|
|
619
|
+
.filter(([, count]) => count >= 2)
|
|
620
|
+
.sort((a, b) => b[1] - a[1])
|
|
621
|
+
.map(([v, count]) => {
|
|
622
|
+
const vNorm = v.trim().toLowerCase();
|
|
623
|
+
// Mark if this issue only appears in failure rounds (possible dead end)
|
|
624
|
+
if (failureViolations.has(vNorm) && !successViolations.has(vNorm)) {
|
|
625
|
+
return `${v} (appeared in ${count} rounds) ⚠️ [Possible dead end — only in failed rounds]`;
|
|
626
|
+
}
|
|
627
|
+
return `${v} (appeared in ${count} rounds)`;
|
|
628
|
+
}));
|
|
629
|
+
}
|
|
324
630
|
return makeRollingSummary({
|
|
325
631
|
quality_trajectory: trajectory,
|
|
326
632
|
trajectory_direction: direction,
|
|
@@ -329,9 +635,10 @@ function buildRollingSummary(loopId, currentRound, vaultContext) {
|
|
|
329
635
|
key_lessons: keyLessons,
|
|
330
636
|
rounds_sampled: sampled.length,
|
|
331
637
|
generated_at_round: currentRound,
|
|
638
|
+
failed_patterns: failurePatterns.length ? failurePatterns : [],
|
|
332
639
|
});
|
|
333
640
|
}
|
|
334
|
-
function formatRollingSummaryForPrompt(rs) {
|
|
641
|
+
export function formatRollingSummaryForPrompt(rs) {
|
|
335
642
|
if (rs === null || rs.rounds_sampled === 0)
|
|
336
643
|
return "";
|
|
337
644
|
const lines = [
|
|
@@ -353,6 +660,18 @@ function formatRollingSummaryForPrompt(rs) {
|
|
|
353
660
|
lines.push(`- ⚠️ ${ri}`);
|
|
354
661
|
lines.push("");
|
|
355
662
|
}
|
|
663
|
+
// v1.7: Failure patterns — explicit warnings about repeated failure paths
|
|
664
|
+
if (rs.failed_patterns && rs.failed_patterns.length) {
|
|
665
|
+
lines.push("### ⚠️ Failure Patterns — Consider Strategy Change");
|
|
666
|
+
lines.push("");
|
|
667
|
+
lines.push("The following approaches failed repeatedly. The compiler has demoted " +
|
|
668
|
+
"their influence on key lessons. Consider a different technique or " +
|
|
669
|
+
"narrower scope.");
|
|
670
|
+
for (const fp of rs.failed_patterns) {
|
|
671
|
+
lines.push(`- 🚫 ${fp}`);
|
|
672
|
+
}
|
|
673
|
+
lines.push("");
|
|
674
|
+
}
|
|
356
675
|
if (rs.key_lessons.length) {
|
|
357
676
|
lines.push("**Key Lessons From High-Score Rounds**:");
|
|
358
677
|
for (const kl of rs.key_lessons)
|
|
@@ -362,6 +681,36 @@ function formatRollingSummaryForPrompt(rs) {
|
|
|
362
681
|
return lines.join("\n");
|
|
363
682
|
}
|
|
364
683
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
684
|
+
// External Context formatting (v1.7 — memory system integration)
|
|
685
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
686
|
+
/** Format an external context string for injection into an L2 prompt.
|
|
687
|
+
* Wraps the raw context in a marked section with a priority disclaimer.
|
|
688
|
+
* Returns empty string if context is empty or injection is disabled by policy. */
|
|
689
|
+
export function formatExternalContext(externalContext, sectionTitle, maxLength) {
|
|
690
|
+
if (!externalContext || externalContext.trim().length === 0)
|
|
691
|
+
return "";
|
|
692
|
+
const trimmed = externalContext.trim().slice(0, maxLength);
|
|
693
|
+
const truncationNote = trimmed.length < externalContext.trim().length
|
|
694
|
+
? `\n> ⚠️ Context was truncated to ${maxLength} characters.`
|
|
695
|
+
: "";
|
|
696
|
+
return [
|
|
697
|
+
"",
|
|
698
|
+
sectionTitle,
|
|
699
|
+
"",
|
|
700
|
+
"> ⚠️ **Role**: Situational awareness only. These are point-in-time",
|
|
701
|
+
"> observations from past sessions — claims about code locations or",
|
|
702
|
+
"> behaviour may be outdated. If any insight contradicts the Loop",
|
|
703
|
+
"> Objective or Active Constraints above, the LoopForge specification",
|
|
704
|
+
"> takes absolute precedence.",
|
|
705
|
+
"",
|
|
706
|
+
trimmed,
|
|
707
|
+
truncationNote,
|
|
708
|
+
"",
|
|
709
|
+
"---",
|
|
710
|
+
"",
|
|
711
|
+
].join("\n");
|
|
712
|
+
}
|
|
713
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
365
714
|
// LAYER 1 — Hard Gates (can change compile level)
|
|
366
715
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
367
716
|
export function decideLevel(request, vaultContext) {
|
|
@@ -384,7 +733,23 @@ export function decideLevel(request, vaultContext) {
|
|
|
384
733
|
// Gate 3: goal_id stability
|
|
385
734
|
if (goalId !== prev.goal_id)
|
|
386
735
|
return "l2";
|
|
387
|
-
// Gate 4:
|
|
736
|
+
// Gate 4: P0–P5 cognitive evolution signals → full recompile (v1.7)
|
|
737
|
+
// Check BEFORE the simple repair/new-constraint gates because evolution signals
|
|
738
|
+
// (constraint discovery, objective refinement, self-correction) need full L2
|
|
739
|
+
// processing. Only structural evolution signals trigger L2 — execution_evidence
|
|
740
|
+
// alone does not (it is processed via the rolling summary at L1).
|
|
741
|
+
if (request.last_round_result) {
|
|
742
|
+
const lr = request.last_round_result;
|
|
743
|
+
if ((lr.discovered_constraints?.length ?? 0) > 0 ||
|
|
744
|
+
(lr.objective_refinement?.trim().length ?? 0) > 0 ||
|
|
745
|
+
(lr.emerged_subtasks?.length ?? 0) > 0 ||
|
|
746
|
+
(lr.retracted_constraints?.length ?? 0) > 0 ||
|
|
747
|
+
(lr.revised_success_criteria?.length ?? 0) > 0 ||
|
|
748
|
+
(lr.wrong_assumptions?.length ?? 0) > 0) {
|
|
749
|
+
return "l2";
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Gate 5: Explicit failures or new constraints → patch
|
|
388
753
|
const hasNewConstraints = request.constraints_from_plan.length > 0;
|
|
389
754
|
const hasNewFailures = request.last_round_result !== null &&
|
|
390
755
|
!request.last_round_result.success;
|
|
@@ -601,7 +966,8 @@ function extractObjectiveFromPlan(planPath) {
|
|
|
601
966
|
const goalPatterns = ["goal", "objective", "目标", "目的", "意图"];
|
|
602
967
|
const successPatterns = [
|
|
603
968
|
"success criteria", "acceptance criteria", "验收标准", "成功标准",
|
|
604
|
-
"done when", "完成标准", "交付标准",
|
|
969
|
+
"done when", "完成标准", "交付标准", "outcome", "deliverable",
|
|
970
|
+
"结果", "产出", "交付物",
|
|
605
971
|
];
|
|
606
972
|
const constraintPatterns = [
|
|
607
973
|
"hard constraint", "constraint", "non-goal", "out of scope",
|
|
@@ -641,6 +1007,12 @@ function extractObjectiveFromPlan(planPath) {
|
|
|
641
1007
|
sections.goal.push(stripped);
|
|
642
1008
|
}
|
|
643
1009
|
}
|
|
1010
|
+
else if (stripped && currentSection === "success" && stripped.length > 10) {
|
|
1011
|
+
// P3: Also capture free-text paragraphs under success criteria headers
|
|
1012
|
+
if (!/^#/.test(stripped)) {
|
|
1013
|
+
sections.success.push(stripped);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
644
1016
|
}
|
|
645
1017
|
if (!Object.values(sections).some((v) => v.length))
|
|
646
1018
|
return null;
|
|
@@ -656,16 +1028,33 @@ function computeLoopObjectiveFromTask(request, _vaultContext) {
|
|
|
656
1028
|
let objective = task.trim().slice(0, 200);
|
|
657
1029
|
const successCriteria = [];
|
|
658
1030
|
const hardConstraints = [...constraints];
|
|
1031
|
+
// P3: Outcome-oriented default success criteria — describe the RESULT,
|
|
1032
|
+
// not the process. The process emerges from the loop.
|
|
659
1033
|
if (/test|测试/i.test(task)) {
|
|
660
|
-
successCriteria.push("All tests pass");
|
|
1034
|
+
successCriteria.push("All tests pass with no regressions");
|
|
661
1035
|
}
|
|
662
1036
|
if (/compat|兼容/i.test(task)) {
|
|
663
|
-
successCriteria.push("
|
|
1037
|
+
successCriteria.push("Existing API contracts and integrations unchanged");
|
|
664
1038
|
}
|
|
665
1039
|
if (/security|安全|audit/i.test(task)) {
|
|
666
|
-
successCriteria.push("
|
|
1040
|
+
successCriteria.push("All identified vulnerabilities resolved or documented");
|
|
667
1041
|
}
|
|
668
|
-
|
|
1042
|
+
if (/fix|修复|bug/i.test(task)) {
|
|
1043
|
+
successCriteria.push("Root cause addressed and verified, not just symptoms");
|
|
1044
|
+
}
|
|
1045
|
+
if (/build|create|implement|开发|实现|构建/i.test(task)) {
|
|
1046
|
+
successCriteria.push("Deliverable matches specification and passes review");
|
|
1047
|
+
}
|
|
1048
|
+
if (/refactor|重构/i.test(task)) {
|
|
1049
|
+
successCriteria.push("Behavior preserved, structure improved, no new failures");
|
|
1050
|
+
}
|
|
1051
|
+
if (/migrate|迁移|upgrade|升级/i.test(task)) {
|
|
1052
|
+
successCriteria.push("Migration completes without data loss or downtime");
|
|
1053
|
+
}
|
|
1054
|
+
if (/perf|performance|optimize|性能|优化/i.test(task)) {
|
|
1055
|
+
successCriteria.push("Measurable improvement verified by benchmarks");
|
|
1056
|
+
}
|
|
1057
|
+
// P3: Try to extract richer objective from plan_source
|
|
669
1058
|
if (request.plan_source) {
|
|
670
1059
|
const planExtracted = extractObjectiveFromPlan(request.plan_source);
|
|
671
1060
|
if (planExtracted) {
|
|
@@ -683,11 +1072,27 @@ function computeLoopObjectiveFromTask(request, _vaultContext) {
|
|
|
683
1072
|
hardConstraints.push(`Follow plan: ${request.plan_source}`);
|
|
684
1073
|
}
|
|
685
1074
|
}
|
|
1075
|
+
// P3: Derive domain from task content for context-aware defaults
|
|
686
1076
|
if (!successCriteria.length) {
|
|
687
|
-
|
|
1077
|
+
if (/\.sol|solidity|contract|智能合约/i.test(task)) {
|
|
1078
|
+
successCriteria.push("All functions respect check-effects-interactions pattern");
|
|
1079
|
+
successCriteria.push("No unchecked external calls or reentrancy vectors");
|
|
1080
|
+
}
|
|
1081
|
+
else if (/\.tsx?|\.jsx?|react|component|组件/i.test(task)) {
|
|
1082
|
+
successCriteria.push("Component renders correctly across all states (loading, empty, error)");
|
|
1083
|
+
successCriteria.push("No prop-type or accessibility regressions");
|
|
1084
|
+
}
|
|
1085
|
+
else if (/api|endpoint|接口|route/i.test(task)) {
|
|
1086
|
+
successCriteria.push("Endpoint responds correctly for all defined status codes");
|
|
1087
|
+
successCriteria.push("Error responses follow the project's error schema");
|
|
1088
|
+
}
|
|
1089
|
+
else {
|
|
1090
|
+
successCriteria.push("Task goal achieved with verifiable evidence");
|
|
1091
|
+
}
|
|
688
1092
|
}
|
|
689
1093
|
if (!hardConstraints.length) {
|
|
690
|
-
hardConstraints.push("Do not modify files outside scope");
|
|
1094
|
+
hardConstraints.push("Do not modify files outside the stated scope");
|
|
1095
|
+
hardConstraints.push("Preserve existing test coverage — no test deletions without replacement");
|
|
691
1096
|
}
|
|
692
1097
|
return makeLoopObjective({
|
|
693
1098
|
objective,
|
|
@@ -695,6 +1100,8 @@ function computeLoopObjectiveFromTask(request, _vaultContext) {
|
|
|
695
1100
|
hard_constraints: hardConstraints,
|
|
696
1101
|
created_at_round: 1,
|
|
697
1102
|
loop_id: request.loop_id,
|
|
1103
|
+
version: 1,
|
|
1104
|
+
refinement_history: [],
|
|
698
1105
|
});
|
|
699
1106
|
}
|
|
700
1107
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -706,7 +1113,7 @@ function compileL0(request, vaultContext, prevRound) {
|
|
|
706
1113
|
const cachedPrompt = prev?.prompt_text ?? "";
|
|
707
1114
|
if (cachedPrompt) {
|
|
708
1115
|
return makeLoopCompileResponse({
|
|
709
|
-
status:
|
|
1116
|
+
status: AgentStatus.OK,
|
|
710
1117
|
prompt: cachedPrompt,
|
|
711
1118
|
recompile_level: "l0",
|
|
712
1119
|
diff_from_previous: `L0 cache hit — reusing prompt from round ${request.round - 1}`,
|
|
@@ -800,7 +1207,7 @@ function compileL1(request, vaultContext, prevRound) {
|
|
|
800
1207
|
lines.push("### Task");
|
|
801
1208
|
lines.push(request.task);
|
|
802
1209
|
return makeLoopCompileResponse({
|
|
803
|
-
status:
|
|
1210
|
+
status: AgentStatus.OK,
|
|
804
1211
|
prompt: lines.join("\n"),
|
|
805
1212
|
recompile_level: "l1",
|
|
806
1213
|
diff_from_previous: diffParts.length ? diffParts.join("; ") : "Patch applied.",
|
|
@@ -825,8 +1232,19 @@ export function compileL2(request, vaultContext) {
|
|
|
825
1232
|
const referenceFile = analysis.reference_file;
|
|
826
1233
|
// v3.5: Rolling summary
|
|
827
1234
|
const rollingSummary = buildRollingSummary(request.loop_id, request.round, vaultContext);
|
|
1235
|
+
// P5c: Wrong assumptions — must be applied BEFORE formatting so they appear
|
|
1236
|
+
// in the rendered cross-round summary text injected into the prompt.
|
|
1237
|
+
const lr = request.last_round_result;
|
|
1238
|
+
if (lr?.wrong_assumptions?.length) {
|
|
1239
|
+
if (rollingSummary) {
|
|
1240
|
+
rollingSummary.key_lessons = [
|
|
1241
|
+
...rollingSummary.key_lessons,
|
|
1242
|
+
...lr.wrong_assumptions.map((a) => `Wrong assumption corrected: ${a}`),
|
|
1243
|
+
];
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
828
1246
|
const rollingText = formatRollingSummaryForPrompt(rollingSummary);
|
|
829
|
-
//
|
|
1247
|
+
// ── Loop Objective: create at round 1, load from vault at later rounds ──
|
|
830
1248
|
let loopObjective = null;
|
|
831
1249
|
if (request.round === 1) {
|
|
832
1250
|
if (request.loop_objective) {
|
|
@@ -836,9 +1254,113 @@ export function compileL2(request, vaultContext) {
|
|
|
836
1254
|
loopObjective = computeLoopObjectiveFromTask(request, vaultContext);
|
|
837
1255
|
}
|
|
838
1256
|
}
|
|
1257
|
+
else {
|
|
1258
|
+
// P1 fix: Load loop objective from vault for round > 1
|
|
1259
|
+
const vaultLo = vaultGetLoopObjective(request.loop_id, vaultContext);
|
|
1260
|
+
if (vaultLo) {
|
|
1261
|
+
const loData = vaultLo.loop_objective;
|
|
1262
|
+
if (loData) {
|
|
1263
|
+
loopObjective = makeLoopObjective({
|
|
1264
|
+
objective: loData.objective ?? "",
|
|
1265
|
+
success_criteria: loData.success_criteria ?? [],
|
|
1266
|
+
hard_constraints: loData.hard_constraints ?? [],
|
|
1267
|
+
created_at_round: loData.created_at_round ?? 1,
|
|
1268
|
+
loop_id: loData.loop_id ?? request.loop_id,
|
|
1269
|
+
version: loData.version ?? 1,
|
|
1270
|
+
refinement_history: loData.refinement_history ?? [],
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (!loopObjective && request.loop_objective) {
|
|
1275
|
+
loopObjective = request.loop_objective;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
// ── P1: Apply objective_refinement from last round ──
|
|
1279
|
+
if (loopObjective && request.last_round_result?.objective_refinement) {
|
|
1280
|
+
const refinement = request.last_round_result.objective_refinement.trim();
|
|
1281
|
+
if (refinement) {
|
|
1282
|
+
const version = (loopObjective.version ?? 1) + 1;
|
|
1283
|
+
const history = [...(loopObjective.refinement_history ?? []), refinement];
|
|
1284
|
+
// Enforce max versions from policy
|
|
1285
|
+
const policy = getPolicy();
|
|
1286
|
+
const maxVersions = policy.evolution?.max_objective_versions ?? 10;
|
|
1287
|
+
const prunedHistory = history.slice(-maxVersions);
|
|
1288
|
+
const refinedObjective = `${loopObjective.objective} [R${request.round}: ${refinement}]`;
|
|
1289
|
+
loopObjective = makeLoopObjective({
|
|
1290
|
+
...loopObjective,
|
|
1291
|
+
objective: refinedObjective,
|
|
1292
|
+
version,
|
|
1293
|
+
refinement_history: prunedHistory,
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
// ── Build active constraints (base + LO + P0 discovered) ──
|
|
839
1298
|
let constraints = [...request.constraints_from_plan];
|
|
840
1299
|
if (loopObjective) {
|
|
841
|
-
constraints = [
|
|
1300
|
+
constraints = [
|
|
1301
|
+
...new Set([
|
|
1302
|
+
...constraints,
|
|
1303
|
+
...loopObjective.hard_constraints,
|
|
1304
|
+
...loopObjective.success_criteria,
|
|
1305
|
+
]),
|
|
1306
|
+
];
|
|
1307
|
+
}
|
|
1308
|
+
// P0: Merge discovered constraints from last round
|
|
1309
|
+
if (request.last_round_result?.discovered_constraints?.length) {
|
|
1310
|
+
const policy = getPolicy();
|
|
1311
|
+
const maxPerRound = policy.evolution?.max_discovered_constraints_per_round ?? 5;
|
|
1312
|
+
const maxActive = policy.evolution?.max_active_constraints ?? 15;
|
|
1313
|
+
const newConstraints = request.last_round_result.discovered_constraints
|
|
1314
|
+
.slice(0, maxPerRound)
|
|
1315
|
+
.filter((c) => !constraints.includes(c));
|
|
1316
|
+
constraints = [...constraints, ...newConstraints].slice(0, maxActive);
|
|
1317
|
+
}
|
|
1318
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1319
|
+
// P5: Self-correction — MUST process retractions/revisions BEFORE prompt build
|
|
1320
|
+
// so the technician compiler sees the corrected constraints and loop objective.
|
|
1321
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1322
|
+
const warnings = [];
|
|
1323
|
+
const retired = [];
|
|
1324
|
+
// P5a: Retracted constraints — remove from active set
|
|
1325
|
+
if (lr?.retracted_constraints?.length) {
|
|
1326
|
+
for (const rc of lr.retracted_constraints) {
|
|
1327
|
+
const idx = constraints.indexOf(rc);
|
|
1328
|
+
if (idx >= 0) {
|
|
1329
|
+
constraints.splice(idx, 1);
|
|
1330
|
+
retired.push(`[agent-retracted] ${rc}`);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
// P5b: Revised success criteria — update Loop Objective
|
|
1335
|
+
if (lr?.revised_success_criteria?.length && loopObjective) {
|
|
1336
|
+
const sc = [...loopObjective.success_criteria];
|
|
1337
|
+
for (const rev of lr.revised_success_criteria) {
|
|
1338
|
+
const idx = sc.indexOf(rev.old);
|
|
1339
|
+
if (idx >= 0) {
|
|
1340
|
+
sc[idx] = rev.new;
|
|
1341
|
+
}
|
|
1342
|
+
else {
|
|
1343
|
+
sc.push(rev.new);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
const version = (loopObjective.version ?? 1) + 1;
|
|
1347
|
+
const history = [
|
|
1348
|
+
...(loopObjective.refinement_history ?? []),
|
|
1349
|
+
`R${request.round}: revised ${lr.revised_success_criteria.length} success criteria`,
|
|
1350
|
+
];
|
|
1351
|
+
const updatedLo = makeLoopObjective({
|
|
1352
|
+
...loopObjective,
|
|
1353
|
+
success_criteria: sc,
|
|
1354
|
+
version,
|
|
1355
|
+
refinement_history: history,
|
|
1356
|
+
});
|
|
1357
|
+
// Rebuild constraints to include revised criteria
|
|
1358
|
+
constraints = [...new Set([
|
|
1359
|
+
...constraints.filter((c) => !updatedLo.hard_constraints.includes(c) && !updatedLo.success_criteria.includes(c)),
|
|
1360
|
+
...updatedLo.hard_constraints,
|
|
1361
|
+
...updatedLo.success_criteria,
|
|
1362
|
+
])];
|
|
1363
|
+
loopObjective = updatedLo;
|
|
842
1364
|
}
|
|
843
1365
|
// ── Route to technique-specific specialist (v1.1 deep integration) ──
|
|
844
1366
|
let prompt;
|
|
@@ -854,16 +1376,131 @@ export function compileL2(request, vaultContext) {
|
|
|
854
1376
|
else {
|
|
855
1377
|
prompt = compileGeneric(request, goalId, constraints, loopObjective, rollingText, analysis, referenceFile, technique);
|
|
856
1378
|
}
|
|
1379
|
+
// ── v1.7: Inject external memory context (L2 only) ──
|
|
1380
|
+
const policy = getPolicy();
|
|
1381
|
+
if (policy.memory_injection.enabled && request.external_context) {
|
|
1382
|
+
prompt += formatExternalContext(request.external_context, policy.memory_injection.section_title, policy.memory_injection.max_context_length);
|
|
1383
|
+
}
|
|
1384
|
+
// ── P2: Emerged subtasks → suggested next task ──
|
|
1385
|
+
let suggestedNextTask = "";
|
|
1386
|
+
if (request.last_round_result?.emerged_subtasks?.length) {
|
|
1387
|
+
suggestedNextTask = request.last_round_result.emerged_subtasks
|
|
1388
|
+
.slice(0, 3)
|
|
1389
|
+
.join("; ");
|
|
1390
|
+
}
|
|
1391
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1392
|
+
// P4: Consistency validation — cross-check agent claims
|
|
1393
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1394
|
+
const evidence = lr?.execution_evidence;
|
|
1395
|
+
if (evidence) {
|
|
1396
|
+
// Check 1: Agent claims action but reports no files changed
|
|
1397
|
+
if (evidence.files_changed.length === 0 && lr.output_summary) {
|
|
1398
|
+
const actionWords = /\b(fix|modif|chang|updat|refactor|implement|add|remov|delet|rewrit|修补|修改|更改|更新|重构|实现|添加|删除|重写)\b/i;
|
|
1399
|
+
if (actionWords.test(lr.output_summary)) {
|
|
1400
|
+
warnings.push("P4: Agent output claims changes but execution_evidence.files_changed is empty — possible oversight");
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
// Check 2: Agent reports success but tests failed
|
|
1404
|
+
if (evidence.test_results && evidence.test_results.failed > 0 && lr.success) {
|
|
1405
|
+
warnings.push(`P4: Agent reports success=true but ${evidence.test_results.failed} tests failed — claims may not match reality`);
|
|
1406
|
+
}
|
|
1407
|
+
// Check 3: Progress estimate mismatch with criteria tracking
|
|
1408
|
+
if (loopObjective && loopObjective.success_criteria.length > 0) {
|
|
1409
|
+
const metCount = evidence.success_criteria_met.length;
|
|
1410
|
+
const totalCount = loopObjective.success_criteria.length;
|
|
1411
|
+
const objectiveProgress = totalCount > 0 ? metCount / totalCount : 0;
|
|
1412
|
+
const subjectiveProgress = evidence.progress_estimate;
|
|
1413
|
+
const policy = getPolicy();
|
|
1414
|
+
const mismatchThreshold = policy.evolution?.progress_mismatch_threshold ?? 0.3;
|
|
1415
|
+
if (Math.abs(objectiveProgress - subjectiveProgress) > mismatchThreshold) {
|
|
1416
|
+
warnings.push(`P4: Progress estimate mismatch — agent estimates ${(subjectiveProgress * 100).toFixed(0)}% but ${metCount}/${totalCount} criteria met (${(objectiveProgress * 100).toFixed(0)}%)`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1421
|
+
// P4: Progress gradient — early stall detection
|
|
1422
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1423
|
+
if (evidence && vaultContext) {
|
|
1424
|
+
const prevEvidence = getPreviousRoundEvidence(request.loop_id, request.round - 1, vaultContext);
|
|
1425
|
+
if (prevEvidence) {
|
|
1426
|
+
const delta = evidence.progress_estimate - prevEvidence.progress_estimate;
|
|
1427
|
+
const policy = getPolicy();
|
|
1428
|
+
const stallThreshold = policy.evolution?.progress_stall_threshold ?? 0.05;
|
|
1429
|
+
const stallRounds = policy.evolution?.progress_stall_rounds ?? 2;
|
|
1430
|
+
if (delta < stallThreshold && evidence.progress_estimate < 0.95) {
|
|
1431
|
+
// Check how many consecutive rounds have been below threshold
|
|
1432
|
+
const stallCount = countProgressStallRounds(request.loop_id, request.round, vaultContext, stallThreshold);
|
|
1433
|
+
if (stallCount >= stallRounds) {
|
|
1434
|
+
warnings.push(`P4: Progress has stalled — <${(stallThreshold * 100).toFixed(0)}% improvement for ${stallCount} consecutive rounds. Current: ${(evidence.progress_estimate * 100).toFixed(0)}%`);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1440
|
+
// P4: Build Progress Dashboard for prompt injection
|
|
1441
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1442
|
+
let progressDashboard = "";
|
|
1443
|
+
if (evidence && loopObjective) {
|
|
1444
|
+
const totalCriteria = loopObjective.success_criteria.length;
|
|
1445
|
+
const metCriteria = evidence.success_criteria_met.length;
|
|
1446
|
+
const remainingCriteria = evidence.success_criteria_remaining;
|
|
1447
|
+
const objProgress = totalCriteria > 0 ? metCriteria / totalCriteria : evidence.progress_estimate;
|
|
1448
|
+
const subjProgress = evidence.progress_estimate;
|
|
1449
|
+
const lines = [
|
|
1450
|
+
"",
|
|
1451
|
+
"### Progress Dashboard",
|
|
1452
|
+
"",
|
|
1453
|
+
`**Overall**: ${metCriteria}/${totalCriteria} criteria met (${(objProgress * 100).toFixed(0)}%)`,
|
|
1454
|
+
];
|
|
1455
|
+
if (Math.abs(objProgress - subjProgress) > 0.05) {
|
|
1456
|
+
lines.push(`**Agent estimate**: ${(subjProgress * 100).toFixed(0)}%`);
|
|
1457
|
+
}
|
|
1458
|
+
if (evidence.files_changed.length) {
|
|
1459
|
+
lines.push(`**Files changed**: ${evidence.files_changed.join(", ")}`);
|
|
1460
|
+
}
|
|
1461
|
+
if (evidence.test_results) {
|
|
1462
|
+
const tr = evidence.test_results;
|
|
1463
|
+
lines.push(`**Tests**: ${tr.passed} passed, ${tr.failed} failed, ${tr.skipped} skipped`);
|
|
1464
|
+
}
|
|
1465
|
+
if (remainingCriteria.length) {
|
|
1466
|
+
lines.push("**Remaining criteria**:");
|
|
1467
|
+
for (const rc of remainingCriteria.slice(0, 5)) {
|
|
1468
|
+
lines.push(`- ${rc}`);
|
|
1469
|
+
}
|
|
1470
|
+
if (remainingCriteria.length > 5) {
|
|
1471
|
+
lines.push(`- ... and ${remainingCriteria.length - 5} more`);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
lines.push("");
|
|
1475
|
+
// Compute trend from previous round
|
|
1476
|
+
if (vaultContext) {
|
|
1477
|
+
const prevEvidence = getPreviousRoundEvidence(request.loop_id, request.round - 1, vaultContext);
|
|
1478
|
+
if (prevEvidence) {
|
|
1479
|
+
const trend = objProgress - (prevEvidence.success_criteria_met.length / Math.max(totalCriteria, 1));
|
|
1480
|
+
if (trend > 0.03) {
|
|
1481
|
+
lines.push(`**Trend**: ↑ advancing (+${(trend * 100).toFixed(0)}% this round)`);
|
|
1482
|
+
}
|
|
1483
|
+
else if (trend > -0.03) {
|
|
1484
|
+
lines.push("**Trend**: → stable");
|
|
1485
|
+
}
|
|
1486
|
+
else {
|
|
1487
|
+
lines.push(`**Trend**: ↓ regressing (${(trend * 100).toFixed(0)}% this round)`);
|
|
1488
|
+
}
|
|
1489
|
+
lines.push("");
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
progressDashboard = lines.join("\n");
|
|
1493
|
+
}
|
|
857
1494
|
return makeLoopCompileResponse({
|
|
858
|
-
status:
|
|
859
|
-
prompt,
|
|
1495
|
+
status: AgentStatus.OK,
|
|
1496
|
+
prompt: prompt + progressDashboard,
|
|
860
1497
|
recompile_level: "l2",
|
|
861
1498
|
diff_from_previous: request.round === 1 || request.plan_source
|
|
862
1499
|
? "Full recompile — new goal or first call."
|
|
863
1500
|
: "Full recompile — goal_id changed or strategy collapse.",
|
|
864
1501
|
lineage: [`${request.loop_id}:r${request.round}`],
|
|
865
1502
|
constraints_active: constraints,
|
|
866
|
-
constraints_retired:
|
|
1503
|
+
constraints_retired: retired,
|
|
867
1504
|
technique_used: technique,
|
|
868
1505
|
reference_file: referenceFile,
|
|
869
1506
|
rolling_summary: rollingSummary,
|
|
@@ -873,7 +1510,8 @@ export function compileL2(request, vaultContext) {
|
|
|
873
1510
|
goal_text_hash: computeGoalTextHash(request.task),
|
|
874
1511
|
loop_objective: loopObjective,
|
|
875
1512
|
plan_source: request.plan_source,
|
|
876
|
-
|
|
1513
|
+
suggested_next_task: suggestedNextTask,
|
|
1514
|
+
warnings,
|
|
877
1515
|
});
|
|
878
1516
|
}
|
|
879
1517
|
function buildHeader(ctx, technique) {
|
|
@@ -1207,7 +1845,8 @@ function compileToT(request, goalId, constraints, loopObjective, rollingText, an
|
|
|
1207
1845
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1208
1846
|
/** Build the standardized self-evaluation block appended to every compiled prompt.
|
|
1209
1847
|
* The agent MUST output a JSON self-evaluation between the delimiters.
|
|
1210
|
-
*
|
|
1848
|
+
* 4 required fields + 3 optional evolution fields (P0–P2) —
|
|
1849
|
+
* each consumed by at least one downstream function. */
|
|
1211
1850
|
export function buildSelfEvalBlock(round) {
|
|
1212
1851
|
return [
|
|
1213
1852
|
"",
|
|
@@ -1222,7 +1861,20 @@ export function buildSelfEvalBlock(round) {
|
|
|
1222
1861
|
' "success": true,',
|
|
1223
1862
|
` "output_summary": "<one paragraph — what was DONE in round ${round}, be specific>",`,
|
|
1224
1863
|
' "constraint_violations": [],',
|
|
1225
|
-
' "should_continue": true',
|
|
1864
|
+
' "should_continue": true,',
|
|
1865
|
+
' "discovered_constraints": [],',
|
|
1866
|
+
' "objective_refinement": "",',
|
|
1867
|
+
' "emerged_subtasks": [],',
|
|
1868
|
+
' "execution_evidence": {',
|
|
1869
|
+
' "files_changed": [],',
|
|
1870
|
+
' "test_results": {"passed": 0, "failed": 0, "skipped": 0},',
|
|
1871
|
+
' "success_criteria_met": [],',
|
|
1872
|
+
' "success_criteria_remaining": [],',
|
|
1873
|
+
' "progress_estimate": 0.0',
|
|
1874
|
+
' },',
|
|
1875
|
+
' "retracted_constraints": [],',
|
|
1876
|
+
' "revised_success_criteria": [],',
|
|
1877
|
+
' "wrong_assumptions": []',
|
|
1226
1878
|
"}",
|
|
1227
1879
|
"---end-loopforge-eval",
|
|
1228
1880
|
"```",
|
|
@@ -1238,6 +1890,23 @@ export function buildSelfEvalBlock(round) {
|
|
|
1238
1890
|
`- should_continue: false ONLY when the ENTIRE task is complete. ` +
|
|
1239
1891
|
`If there is more to audit/implement/test, say true. ` +
|
|
1240
1892
|
`This tells the autonomous runner when to stop.`,
|
|
1893
|
+
`- discovered_constraints (P0 — optional): New constraints you discovered this round. ` +
|
|
1894
|
+
`Omit or [] if none.`,
|
|
1895
|
+
`- objective_refinement (P1 — optional): Deepened understanding of the task goal. ` +
|
|
1896
|
+
`APPENDED to the original objective. Omit or "" if unchanged.`,
|
|
1897
|
+
`- emerged_subtasks (P2 — optional): Sub-problems that surfaced during execution. ` +
|
|
1898
|
+
`Omit or [] if none.`,
|
|
1899
|
+
`- execution_evidence (P4 — recommended): Structured record of what you actually did. ` +
|
|
1900
|
+
`files_changed: paths relative to project root. test_results: null if no tests run. ` +
|
|
1901
|
+
`success_criteria_met/remaining: track against the Loop Objective. ` +
|
|
1902
|
+
`progress_estimate: your honest estimate of overall completion (0.0 to 1.0). ` +
|
|
1903
|
+
`This enables real progress tracking and early stall detection.`,
|
|
1904
|
+
`- retracted_constraints (P5 — optional): Constraints you now believe are WRONG. ` +
|
|
1905
|
+
`Removed from active guardrails. Only retract if you have evidence. Omit or [] if none.`,
|
|
1906
|
+
`- revised_success_criteria (P5 — optional): Success criteria that need reformulation. ` +
|
|
1907
|
+
`Array of {old, new} objects. Applied to the Loop Objective. Omit or [] if none.`,
|
|
1908
|
+
`- wrong_assumptions (P5 — optional): Assumptions from earlier rounds that turned ` +
|
|
1909
|
+
`out to be incorrect. Recorded as key lessons. Omit or [] if none.`,
|
|
1241
1910
|
`- The JSON MUST appear between the ---loopforge-eval and ---end-loopforge-eval markers`,
|
|
1242
1911
|
`- Do NOT wrap the markers in code fences — output them as raw text`,
|
|
1243
1912
|
"",
|
|
@@ -1262,14 +1931,25 @@ export function compileLoop(request, vaultContext = null) {
|
|
|
1262
1931
|
}
|
|
1263
1932
|
// Layer 2: Compute advisories
|
|
1264
1933
|
const { warnings, suggestedNextTask, alignment, health } = computeAdvisories(request, vaultContext);
|
|
1265
|
-
// Merge advisories into response
|
|
1266
|
-
response.warnings = warnings;
|
|
1267
|
-
|
|
1934
|
+
// Merge advisories into response (preserve P4 consistency warnings from L2)
|
|
1935
|
+
response.warnings = [...(response.warnings || []), ...warnings];
|
|
1936
|
+
// Only overwrite suggested_next_task from advisories if the compile function
|
|
1937
|
+
// didn't already set one (e.g. L2 sets it from emerged_subtasks)
|
|
1938
|
+
if (!response.suggested_next_task) {
|
|
1939
|
+
response.suggested_next_task = suggestedNextTask;
|
|
1940
|
+
}
|
|
1268
1941
|
response.task_alignment = alignment;
|
|
1269
1942
|
response.loop_health = health;
|
|
1270
1943
|
response.recompile_level = level;
|
|
1271
1944
|
// v1.1: Append self-evaluation block for autonomous loop feedback
|
|
1272
1945
|
response.prompt += buildSelfEvalBlock(request.round);
|
|
1946
|
+
// Multi-agent: Inject delegation history if available
|
|
1947
|
+
if (vaultContext) {
|
|
1948
|
+
const delegationText = buildDelegationSummary(vaultContext);
|
|
1949
|
+
if (delegationText) {
|
|
1950
|
+
response.prompt += delegationText;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1273
1953
|
return response;
|
|
1274
1954
|
}
|
|
1275
1955
|
//# sourceMappingURL=loop-compiler.js.map
|