loopforge 1.12.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.md +172 -95
  2. package/dist/backends/interface.d.ts +3 -9
  3. package/dist/backends/interface.d.ts.map +1 -1
  4. package/dist/backends/interface.js +1 -5
  5. package/dist/backends/interface.js.map +1 -1
  6. package/dist/canonical-state.d.ts +53 -0
  7. package/dist/canonical-state.d.ts.map +1 -0
  8. package/dist/canonical-state.js +161 -0
  9. package/dist/canonical-state.js.map +1 -0
  10. package/dist/cli.d.ts +4 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +227 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/enforcement-gate.d.ts +44 -0
  15. package/dist/enforcement-gate.d.ts.map +1 -0
  16. package/dist/enforcement-gate.js +271 -0
  17. package/dist/enforcement-gate.js.map +1 -0
  18. package/dist/engine.d.ts +17 -7
  19. package/dist/engine.d.ts.map +1 -1
  20. package/dist/engine.js +158 -249
  21. package/dist/engine.js.map +1 -1
  22. package/dist/evidence-provider.d.ts +113 -0
  23. package/dist/evidence-provider.d.ts.map +1 -0
  24. package/dist/evidence-provider.js +419 -0
  25. package/dist/evidence-provider.js.map +1 -0
  26. package/dist/index.d.ts +19 -11
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +16 -10
  29. package/dist/index.js.map +1 -1
  30. package/dist/init.d.ts +15 -0
  31. package/dist/init.d.ts.map +1 -0
  32. package/dist/init.js +49 -0
  33. package/dist/init.js.map +1 -0
  34. package/dist/interop.d.ts +30 -0
  35. package/dist/interop.d.ts.map +1 -0
  36. package/dist/interop.js +29 -0
  37. package/dist/interop.js.map +1 -0
  38. package/dist/loop-compiler.d.ts +14 -64
  39. package/dist/loop-compiler.d.ts.map +1 -1
  40. package/dist/loop-compiler.js +307 -2001
  41. package/dist/loop-compiler.js.map +1 -1
  42. package/dist/loop-store.d.ts +82 -0
  43. package/dist/loop-store.d.ts.map +1 -0
  44. package/dist/loop-store.js +332 -0
  45. package/dist/loop-store.js.map +1 -0
  46. package/dist/mcp/server.d.ts +9 -6
  47. package/dist/mcp/server.d.ts.map +1 -1
  48. package/dist/mcp/server.js +109 -73
  49. package/dist/mcp/server.js.map +1 -1
  50. package/dist/mcp/session.d.ts +84 -20
  51. package/dist/mcp/session.d.ts.map +1 -1
  52. package/dist/mcp/session.js +718 -316
  53. package/dist/mcp/session.js.map +1 -1
  54. package/dist/mcp/tools.d.ts +18 -268
  55. package/dist/mcp/tools.d.ts.map +1 -1
  56. package/dist/mcp/tools.js +225 -12
  57. package/dist/mcp/tools.js.map +1 -1
  58. package/dist/observability.d.ts +36 -16
  59. package/dist/observability.d.ts.map +1 -1
  60. package/dist/observability.js +82 -23
  61. package/dist/observability.js.map +1 -1
  62. package/dist/policy-metrics.d.ts +46 -0
  63. package/dist/policy-metrics.d.ts.map +1 -0
  64. package/dist/policy-metrics.js +146 -0
  65. package/dist/policy-metrics.js.map +1 -0
  66. package/dist/policy.d.ts +58 -75
  67. package/dist/policy.d.ts.map +1 -1
  68. package/dist/policy.js +122 -93
  69. package/dist/policy.js.map +1 -1
  70. package/dist/prompt-assembler.d.ts +30 -0
  71. package/dist/prompt-assembler.d.ts.map +1 -0
  72. package/dist/prompt-assembler.js +257 -0
  73. package/dist/prompt-assembler.js.map +1 -0
  74. package/dist/prompt-policy.d.ts +29 -0
  75. package/dist/prompt-policy.d.ts.map +1 -0
  76. package/dist/prompt-policy.js +51 -0
  77. package/dist/prompt-policy.js.map +1 -0
  78. package/dist/protocol.d.ts +117 -91
  79. package/dist/protocol.d.ts.map +1 -1
  80. package/dist/protocol.js +40 -36
  81. package/dist/protocol.js.map +1 -1
  82. package/dist/replay.d.ts.map +1 -1
  83. package/dist/replay.js +1 -27
  84. package/dist/replay.js.map +1 -1
  85. package/dist/round-coordinator.d.ts +90 -0
  86. package/dist/round-coordinator.d.ts.map +1 -0
  87. package/dist/round-coordinator.js +186 -0
  88. package/dist/round-coordinator.js.map +1 -0
  89. package/dist/round-driver.d.ts +53 -0
  90. package/dist/round-driver.d.ts.map +1 -0
  91. package/dist/round-driver.js +101 -0
  92. package/dist/round-driver.js.map +1 -0
  93. package/dist/round-transaction.d.ts +70 -0
  94. package/dist/round-transaction.d.ts.map +1 -0
  95. package/dist/round-transaction.js +290 -0
  96. package/dist/round-transaction.js.map +1 -0
  97. package/dist/runtime.d.ts +35 -17
  98. package/dist/runtime.d.ts.map +1 -1
  99. package/dist/runtime.js +357 -344
  100. package/dist/runtime.js.map +1 -1
  101. package/dist/storage.d.ts +38 -0
  102. package/dist/storage.d.ts.map +1 -0
  103. package/dist/storage.js +201 -0
  104. package/dist/storage.js.map +1 -0
  105. package/dist/verification-gate.d.ts +39 -7
  106. package/dist/verification-gate.d.ts.map +1 -1
  107. package/dist/verification-gate.js +198 -9
  108. package/dist/verification-gate.js.map +1 -1
  109. package/loop_policy.json +26 -47
  110. package/package.json +13 -10
  111. package/skills/perception/SKILL.md +80 -249
  112. package/dist/backends/fs.d.ts +0 -36
  113. package/dist/backends/fs.d.ts.map +0 -1
  114. package/dist/backends/fs.js +0 -358
  115. package/dist/backends/fs.js.map +0 -1
  116. package/dist/builder.d.ts +0 -12
  117. package/dist/builder.d.ts.map +0 -1
  118. package/dist/builder.js +0 -259
  119. package/dist/builder.js.map +0 -1
  120. package/dist/mcp-server.d.ts +0 -12
  121. package/dist/mcp-server.d.ts.map +0 -1
  122. package/dist/mcp-server.js +0 -14
  123. package/dist/mcp-server.js.map +0 -1
  124. package/dist/memory-bridge.d.ts +0 -47
  125. package/dist/memory-bridge.d.ts.map +0 -1
  126. package/dist/memory-bridge.js +0 -290
  127. package/dist/memory-bridge.js.map +0 -1
  128. package/dist/tests/_helpers.d.ts +0 -25
  129. package/dist/tests/_helpers.d.ts.map +0 -1
  130. package/dist/tests/_helpers.js +0 -47
  131. package/dist/tests/_helpers.js.map +0 -1
  132. package/dist/tests/builder.test.d.ts +0 -2
  133. package/dist/tests/builder.test.d.ts.map +0 -1
  134. package/dist/tests/builder.test.js +0 -110
  135. package/dist/tests/builder.test.js.map +0 -1
  136. package/dist/tests/engine.test.d.ts +0 -2
  137. package/dist/tests/engine.test.d.ts.map +0 -1
  138. package/dist/tests/engine.test.js +0 -371
  139. package/dist/tests/engine.test.js.map +0 -1
  140. package/dist/tests/generate-schema.test.d.ts +0 -8
  141. package/dist/tests/generate-schema.test.d.ts.map +0 -1
  142. package/dist/tests/generate-schema.test.js +0 -199
  143. package/dist/tests/generate-schema.test.js.map +0 -1
  144. package/dist/tests/loop-compiler.test.d.ts +0 -2
  145. package/dist/tests/loop-compiler.test.d.ts.map +0 -1
  146. package/dist/tests/loop-compiler.test.js +0 -1051
  147. package/dist/tests/loop-compiler.test.js.map +0 -1
  148. package/dist/tests/mcp.test.d.ts +0 -7
  149. package/dist/tests/mcp.test.d.ts.map +0 -1
  150. package/dist/tests/mcp.test.js +0 -485
  151. package/dist/tests/mcp.test.js.map +0 -1
  152. package/dist/tests/memory-bridge.test.d.ts +0 -8
  153. package/dist/tests/memory-bridge.test.d.ts.map +0 -1
  154. package/dist/tests/memory-bridge.test.js +0 -383
  155. package/dist/tests/memory-bridge.test.js.map +0 -1
  156. package/dist/tests/policy.test.d.ts +0 -2
  157. package/dist/tests/policy.test.d.ts.map +0 -1
  158. package/dist/tests/policy.test.js +0 -49
  159. package/dist/tests/policy.test.js.map +0 -1
  160. package/dist/tests/protocol.test.d.ts +0 -2
  161. package/dist/tests/protocol.test.d.ts.map +0 -1
  162. package/dist/tests/protocol.test.js +0 -73
  163. package/dist/tests/protocol.test.js.map +0 -1
  164. package/dist/tests/replay.test.d.ts +0 -2
  165. package/dist/tests/replay.test.d.ts.map +0 -1
  166. package/dist/tests/replay.test.js +0 -222
  167. package/dist/tests/replay.test.js.map +0 -1
  168. package/dist/tests/runtime.test.d.ts +0 -7
  169. package/dist/tests/runtime.test.d.ts.map +0 -1
  170. package/dist/tests/runtime.test.js +0 -323
  171. package/dist/tests/runtime.test.js.map +0 -1
  172. package/dist/tests/verification-gate.test.d.ts +0 -2
  173. package/dist/tests/verification-gate.test.d.ts.map +0 -1
  174. package/dist/tests/verification-gate.test.js +0 -407
  175. package/dist/tests/verification-gate.test.js.map +0 -1
  176. package/skills/prompt-techniques/SKILL.md +0 -52
  177. package/skills/prompt-techniques/references/chain-of-thought.md +0 -207
  178. package/skills/prompt-techniques/references/few-shot.md +0 -174
  179. package/skills/prompt-techniques/references/least-to-most.md +0 -182
  180. package/skills/prompt-techniques/references/step-back.md +0 -160
  181. package/skills/prompt-techniques/references/tree-of-thought.md +0 -191
  182. package/skills/prompt-techniques/references/zero-shot.md +0 -97
@@ -1,2045 +1,351 @@
1
- /** LoopForge-loop_compile — Loop Compiler (v3.5 core).
1
+ /** LoopForge cognitive-state compiler.
2
2
  *
3
- * Pure-function module for per-loop-iteration prompt compilation.
4
- *
5
- * Two layers:
6
- * Layer 1 (Hard Gates): decideLevel() — 4-gate routing that CAN change compile level.
7
- * Layer 2 (Soft Advisories): computeAdvisories() — warnings/alignment/health, NEVER
8
- * change compile level directly.
9
- *
10
- * Compilation: compileL0() / compileL1() / compileL2() produce the actual prompt.
3
+ * The compiler evolves structured state and renders one prompt artifact.
4
+ * L0/L1/L2 control state density only; the external Agent owns reasoning.
11
5
  */
12
6
  import { createHash } from "node:crypto";
13
- import { readFileSync } from "node:fs";
14
7
  import { getPolicy } from "./policy.js";
15
8
  import { AgentStatus, makeCheckpointSummary, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeRollingSummary, makeTaskAlignment, } from "./protocol.js";
16
- import { routeTechniqueAdaptive } from "./builder.js";
17
- // ═══════════════════════════════════════════════════════════════════════════
18
- // Repair cue detection
19
- // ═══════════════════════════════════════════════════════════════════════════
20
- const REPAIR_CUES = [
21
- "fix", "repair", "revise", "correct", "polish", "bug", "error",
22
- "修复", "修改", "修正", "纠错", "补充", "改一下",
23
- ];
24
- function detectsRepairSignal(request) {
25
- let text = (request.new_since_last_round || "").toLowerCase();
26
- if (request.last_round_result) {
27
- text += " " + (request.last_round_result.output_summary || "").toLowerCase();
28
- if (request.last_round_result.manual_fixes_needed) {
29
- text += " " + request.last_round_result.manual_fixes_needed.toLowerCase();
30
- }
31
- }
32
- return REPAIR_CUES.some((cue) => text.includes(cue));
33
- }
34
- // ═══════════════════════════════════════════════════════════════════════════
35
- // Tokenization helpers
36
- // ═══════════════════════════════════════════════════════════════════════════
37
- export function tokenize(text) {
38
- const tokens = text.split(/\s+/);
39
- const result = new Set();
40
- for (let token of tokens) {
41
- token = token.trim().replace(/^[.,;:!?()[\]{}'"]+|[.,;:!?()[\]{}'"]+$/g, "");
42
- if (token.length >= 2) {
43
- result.add(/^[\x00-\x7F]*$/.test(token) ? token.toLowerCase() : token);
44
- }
45
- // Add individual CJK chars as standalone tokens
46
- for (const ch of token) {
47
- if ((ch >= "一" && ch <= "鿿") ||
48
- (ch >= "぀" && ch <= "ヿ")) {
49
- result.add(ch);
50
- }
51
- }
52
- }
53
- return result;
54
- }
55
- export function jaccard(a, b) {
56
- if (a.size === 0 || b.size === 0)
57
- return 0.0;
58
- let intersection = 0;
59
- for (const item of a) {
60
- if (b.has(item))
61
- intersection++;
62
- }
63
- const union = a.size + b.size - intersection;
64
- return union > 0 ? intersection / union : 0.0;
65
- }
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
- // ═══════════════════════════════════════════════════════════════════════════
202
- // Goal identity
203
- // ═══════════════════════════════════════════════════════════════════════════
204
- export function computeGoalTextHash(task) {
205
- const normalized = (task || "").trim().toLowerCase().replace(/\s+/g, " ");
206
- return createHash("sha256").update(normalized, "utf-8").digest("hex").slice(0, 12);
207
- }
208
- export function deriveGoalId(loopId, task, explicitGoalId = "") {
209
- if (explicitGoalId)
210
- return explicitGoalId;
211
- let taskPrefix = (task || "unnamed").slice(0, 60).trim().toLowerCase();
212
- taskPrefix = taskPrefix.replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
213
- return loopId ? `${loopId}:${taskPrefix}` : taskPrefix;
214
- }
215
- export function getPreviousRound(loopId, roundNum, vaultContext) {
216
- if (vaultContext === null)
217
- return null;
218
- const results = vaultContext.results || [];
219
- if (!results.length)
220
- return null;
221
- for (const r of results) {
222
- const lineage = (r.loop_lineage || r.lineage || {});
223
- if (lineage.loop_id === loopId && lineage.round === roundNum) {
224
- return {
225
- goal_id: lineage.goal_id ?? "",
226
- goal_text_hash: lineage.goal_text_hash ?? "",
227
- success: r.success ?? lineage.success ?? true,
228
- task: r.task ?? r.user_intent ?? "",
229
- constraints_active: lineage.constraints_active ?? [],
230
- prompt_text: r.full_prompt ?? "",
231
- };
232
- }
233
- }
234
- return null;
235
- }
236
- function getRecentRounds(loopId, n, vaultContext) {
237
- if (vaultContext === null)
9
+ import { createCanonicalLoopState, renderCanonicalStateMarkdown, } from "./canonical-state.js";
10
+ import { assemblePromptArtifact } from "./prompt-assembler.js";
11
+ import { decidePromptLevel, } from "./prompt-policy.js";
12
+ function unique(values) {
13
+ return [...new Set(values
14
+ .filter((value) => typeof value === "string")
15
+ .map((value) => value.trim())
16
+ .filter(Boolean))];
17
+ }
18
+ function contextEntries(context) {
19
+ if (!context || !Array.isArray(context.results))
238
20
  return [];
239
- const results = vaultContext.results || [];
240
- const rounds = [];
241
- for (const r of results) {
242
- const lineage = (r.loop_lineage || r.lineage || {});
243
- if (lineage.loop_id === loopId) {
244
- rounds.push({
245
- success: r.success ?? lineage.success ?? false,
246
- round: lineage.round ?? 0,
247
- goal_text_hash: lineage.goal_text_hash ?? "",
248
- });
249
- }
250
- }
251
- rounds.sort((a, b) => b.round - a.round);
252
- return rounds.slice(0, n);
253
- }
254
- function getPreviousRoundTask(loopId, roundNum, vaultContext) {
255
- const prev = getPreviousRound(loopId, roundNum, vaultContext);
256
- return prev?.task ?? "";
257
- }
258
- function vaultGetLoopObjective(loopId, vaultContext) {
259
- if (vaultContext === null)
21
+ return context.results.filter((value) => value !== null && typeof value === "object" && !Array.isArray(value));
22
+ }
23
+ function lineage(entry) {
24
+ const value = entry.loop_lineage ?? entry.lineage;
25
+ return value !== null && typeof value === "object" && !Array.isArray(value)
26
+ ? value
27
+ : {};
28
+ }
29
+ function entryRound(entry) {
30
+ const value = lineage(entry).round;
31
+ return typeof value === "number" ? value : 0;
32
+ }
33
+ function loopEntries(loopId, context) {
34
+ return contextEntries(context)
35
+ .filter((entry) => entry.loop_id === loopId || lineage(entry).loop_id === loopId)
36
+ .sort((a, b) => entryRound(a) - entryRound(b));
37
+ }
38
+ export function computeGoalTextHash(text) {
39
+ const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
40
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
41
+ }
42
+ export function deriveGoalId(loopId, task, explicit = "") {
43
+ return explicit.trim() || `${loopId}:${task.trim().toLowerCase()
44
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-")
45
+ .replace(/^-|-$/g, "")
46
+ .slice(0, 48) || computeGoalTextHash(task)}`;
47
+ }
48
+ export function getPreviousRound(loopId, round, context) {
49
+ const entry = [...loopEntries(loopId, context)].reverse()
50
+ .find((candidate) => entryRound(candidate) === round);
51
+ if (!entry)
260
52
  return null;
261
- // Check global entries first
262
- const globalEntries = vaultContext.global_entries || [];
263
- for (const entry of globalEntries) {
264
- const lo = entry.loop_objective;
265
- if (lo && entry.loop_id === loopId) {
266
- return { loop_objective: lo, loop_id: loopId };
267
- }
268
- }
269
- // Check results
270
- const results = vaultContext.results || [];
271
- for (const r of results) {
272
- const lo = r.loop_objective;
273
- if (lo && r.loop_id === loopId) {
274
- return { loop_objective: lo, loop_id: loopId };
275
- }
276
- }
277
- return null;
278
- }
279
- function countConsecutiveHashMismatches(loopId, vaultContext) {
280
- if (vaultContext === null)
281
- return 0;
282
- const rounds = getRecentRounds(loopId, 20, vaultContext);
283
- if (rounds.length < 2)
284
- return 0;
285
- let count = 0;
286
- for (let i = 0; i < rounds.length - 1; i++) {
287
- const currHash = rounds[i].goal_text_hash;
288
- const prevHash = rounds[i + 1].goal_text_hash;
289
- if (currHash && prevHash && currHash !== prevHash) {
290
- count++;
291
- }
292
- else {
293
- break;
294
- }
295
- }
296
- return count;
297
- }
298
- // ═══════════════════════════════════════════════════════════════════════════
299
- // Strategy collapse detection
300
- // ═══════════════════════════════════════════════════════════════════════════
301
- function strategyCollapse(loopId, vaultContext) {
302
- const recent = getRecentRounds(loopId, 3, vaultContext);
303
- if (recent.length < 3)
304
- return false;
305
- return recent.every((r) => r.success === false);
306
- }
307
- // ═══════════════════════════════════════════════════════════════════════════
308
- // Constraint Retirement (v3.5)
309
- // ═══════════════════════════════════════════════════════════════════════════
310
- function computeConstraintRetirement(activeConstraints, loopId, currentRound, vaultContext) {
311
- if (!activeConstraints.length || vaultContext === null) {
312
- return { active: [...activeConstraints], retired: [] };
313
- }
314
- const results = vaultContext.results || [];
315
- if (!results.length) {
316
- return { active: [...activeConstraints], retired: [] };
317
- }
318
- const policy = getPolicy();
319
- const window = policy.constraints.retire_window;
320
- const targetRounds = new Set();
321
- for (let r = currentRound - window; r < currentRound; r++) {
322
- targetRounds.add(r);
323
- }
324
- // Collect all text per target round for activity detection
325
- const roundTexts = new Map();
326
- for (const r of results) {
327
- const lineage = (r.loop_lineage || r.lineage || {});
328
- if (lineage.loop_id !== loopId)
329
- continue;
330
- const rnd = lineage.round;
331
- if (!targetRounds.has(rnd))
332
- continue;
333
- let text = ((r.task ?? r.user_intent ?? "") + " " +
334
- (lineage.task ?? "") + " " +
335
- (r.output_summary ?? "")).toLowerCase();
336
- for (const v of r.constraint_violations || []) {
337
- text += " " + String(v).toLowerCase();
338
- }
339
- roundTexts.set(rnd, text);
340
- }
341
- const retired = [];
342
- const pruned = [];
343
- for (const constraint of activeConstraints) {
344
- const cLower = constraint.toLowerCase();
345
- const cNormalized = cLower.replace(/-/g, " ");
346
- let isActive = false;
347
- for (const text of roundTexts.values()) {
348
- if (text.includes(cLower) || text.includes(cNormalized)) {
349
- isActive = true;
350
- break;
351
- }
352
- }
353
- if (isActive || roundTexts.size < window) {
354
- pruned.push(constraint);
355
- }
356
- else {
357
- retired.push(constraint);
358
- }
359
- }
360
- return { active: pruned, retired };
53
+ const data = lineage(entry);
54
+ return {
55
+ round,
56
+ goal_id: typeof data.goal_id === "string" ? data.goal_id : "",
57
+ goal_text_hash: typeof data.goal_text_hash === "string" ? data.goal_text_hash : "",
58
+ success: typeof entry.success === "boolean"
59
+ ? entry.success
60
+ : typeof data.success === "boolean" ? data.success : false,
61
+ task: typeof data.task === "string"
62
+ ? data.task
63
+ : typeof entry.task === "string" ? entry.task : "",
64
+ constraints_active: Array.isArray(data.constraints_active)
65
+ ? data.constraints_active.filter((value) => typeof value === "string")
66
+ : [],
67
+ output_summary: typeof entry.output_summary === "string"
68
+ ? entry.output_summary
69
+ : typeof data.output_summary === "string" ? data.output_summary : "",
70
+ };
361
71
  }
362
- // ═══════════════════════════════════════════════════════════════════════════
363
- // P4: Progress evidence helpers
364
- // ═══════════════════════════════════════════════════════════════════════════
365
- /** Extract execution evidence from a previous round in the vault.
366
- * Used for progress gradient calculation and trend detection. */
367
- function getPreviousRoundEvidence(loopId, roundNum, vaultContext) {
368
- if (vaultContext === null)
369
- return null;
370
- const results = vaultContext.results || [];
371
- for (const r of results) {
372
- const lineage = (r.loop_lineage || r.lineage || {});
373
- if (lineage.loop_id !== loopId)
374
- continue;
375
- if (lineage.round !== roundNum)
72
+ function latestObjective(loopId, context) {
73
+ const candidates = loopEntries(loopId, context).reverse();
74
+ for (const entry of candidates) {
75
+ const raw = entry.loop_objective;
76
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
376
77
  continue;
377
- const evidence = (r.execution_evidence ?? lineage.execution_evidence);
378
- if (evidence) {
379
- return {
380
- progress_estimate: evidence.progress_estimate ?? 0,
381
- success_criteria_met: evidence.success_criteria_met ?? [],
382
- };
383
- }
384
- return null;
78
+ const value = raw;
79
+ return makeLoopObjective({
80
+ objective: typeof value.objective === "string" ? value.objective : "",
81
+ success_criteria: Array.isArray(value.success_criteria)
82
+ ? value.success_criteria.filter((item) => typeof item === "string")
83
+ : [],
84
+ hard_constraints: Array.isArray(value.hard_constraints)
85
+ ? value.hard_constraints.filter((item) => typeof item === "string")
86
+ : [],
87
+ created_at_round: typeof value.created_at_round === "number" ? value.created_at_round : 1,
88
+ loop_id: loopId,
89
+ version: typeof value.version === "number" ? value.version : 1,
90
+ refinement_history: Array.isArray(value.refinement_history)
91
+ ? value.refinement_history.filter((item) => typeof item === "string")
92
+ : [],
93
+ });
385
94
  }
386
95
  return null;
387
96
  }
388
- /** Count consecutive rounds (including current) where progress delta
389
- * has been below the stall threshold. */
390
- function countProgressStallRounds(loopId, currentRound, vaultContext, threshold) {
391
- if (vaultContext === null || currentRound < 2)
392
- return 0;
393
- let count = 0;
394
- for (let r = currentRound; r >= 2; r--) {
395
- const curr = getPreviousRoundEvidence(loopId, r, vaultContext);
396
- const prev = getPreviousRoundEvidence(loopId, r - 1, vaultContext);
397
- if (curr && prev) {
398
- const delta = curr.progress_estimate - prev.progress_estimate;
399
- if (delta < threshold && curr.progress_estimate < 0.95) {
400
- count++;
401
- }
402
- else {
403
- break;
404
- }
405
- }
406
- else {
407
- break;
408
- }
409
- }
410
- return count;
411
- }
412
- /** Detect repeated failure patterns in sampled rounds.
413
- * A failure pattern = 2+ consecutive rounds where:
414
- * 1. success === false (failed)
415
- * 2. same technique was used
416
- * 3. task text is similar (Jaccard > 0.4)
417
- *
418
- * Returns human-readable descriptions for injection into the prompt. */
419
- function detectFailurePatterns(sampled) {
420
- if (sampled.length < 2)
421
- return [];
422
- const patterns = [];
423
- let runStart = -1;
424
- for (let i = 0; i < sampled.length; i++) {
425
- const r = sampled[i];
426
- const isFailed = r.success === false;
427
- if (isFailed && runStart === -1) {
428
- runStart = i;
429
- }
430
- else if (!isFailed && runStart !== -1) {
431
- if (i - runStart >= 2) {
432
- const pattern = classifyRun(sampled.slice(runStart, i));
433
- if (pattern)
434
- patterns.push(pattern);
435
- }
436
- runStart = -1;
437
- }
438
- }
439
- // Handle run at end of array
440
- if (runStart !== -1 && sampled.length - runStart >= 2) {
441
- const pattern = classifyRun(sampled.slice(runStart));
442
- if (pattern)
443
- patterns.push(pattern);
444
- }
445
- return patterns;
446
- }
447
- /** Classify a run of consecutive failed rounds as a failure pattern.
448
- * Returns null if the rounds are too dissimilar to be the same pattern. */
449
- function classifyRun(run) {
450
- if (run.length < 2)
451
- return null;
452
- // Must all use the same technique
453
- const technique = run[0].technique_used || "unknown";
454
- if (!run.every((r) => (r.technique_used || "unknown") === technique)) {
455
- return null;
456
- }
457
- // Task text must be similar across the run (pairwise Jaccard)
458
- const tokensList = run.map((r) => tokenize(r.task.toLowerCase()));
459
- for (let i = 0; i < tokensList.length - 1; i++) {
460
- for (let j = i + 1; j < tokensList.length; j++) {
461
- if (jaccard(tokensList[i], tokensList[j]) < 0.4)
462
- return null;
463
- }
464
- }
465
- const rounds = `R${run[0].round}-R${run[run.length - 1].round}`;
466
- const taskPreview = run[0].task.slice(0, 80);
467
- const failureRate = `${run.length}/${run.length}`;
468
- return (`technique '${technique}' on task '${taskPreview}' ` +
469
- `failed ${run.length} consecutive rounds (${rounds}) — ` +
470
- `consider strategy change`);
471
- }
472
- /** Extract round numbers referenced in failure pattern descriptions.
473
- * Used by buildRollingSummary to identify which rounds to demote. */
474
- function extractFailureRoundNums(patterns) {
475
- const nums = new Set();
476
- for (const p of patterns) {
477
- // Match "R2-R4" or "R5"
478
- const rangeMatch = p.match(/R(\d+)-R(\d+)/);
479
- if (rangeMatch) {
480
- const start = parseInt(rangeMatch[1], 10);
481
- const end = parseInt(rangeMatch[2], 10);
482
- for (let r = start; r <= end; r++)
483
- nums.add(r);
484
- }
485
- else {
486
- const singleMatch = p.match(/R(\d+)/);
487
- if (singleMatch)
488
- nums.add(parseInt(singleMatch[1], 10));
489
- }
490
- }
491
- return nums;
492
- }
493
- // ═══════════════════════════════════════════════════════════════════════════
494
- // Rolling Summary (v3.5)
495
- // ═══════════════════════════════════════════════════════════════════════════
496
- export function buildRollingSummary(loopId, currentRound, vaultContext, sinceRound) {
497
- if (vaultContext === null)
498
- return null;
499
- const results = vaultContext.results || [];
500
- if (!results.length)
501
- return null;
502
- const policy = getPolicy();
503
- const window = policy.summary.window;
504
- const minRound = sinceRound ?? 0;
505
- // Collect rounds matching this loop_id, excluding current
506
- const rounds = [];
507
- for (const r of results) {
508
- const lineage = (r.loop_lineage || r.lineage || {});
509
- if (lineage.loop_id !== loopId)
510
- continue;
511
- const rnd = lineage.round;
512
- if (rnd >= currentRound)
513
- continue;
514
- if (rnd < minRound)
515
- continue;
516
- rounds.push({
517
- round: rnd,
518
- success: r.success ?? lineage.success ?? false,
519
- task: r.task ?? r.user_intent ?? "",
520
- output_summary: r.output_summary ?? "",
521
- constraint_violations: r.constraint_violations || [],
522
- technique_used: r.technique_used ?? "",
97
+ function evolveObjective(request, context) {
98
+ const current = request.loop_objective ?? latestObjective(request.loop_id, context) ??
99
+ makeLoopObjective({
100
+ objective: request.task,
101
+ success_criteria: ["Task goal achieved with verifiable evidence"],
102
+ hard_constraints: unique(request.constraints_from_plan),
103
+ created_at_round: 1,
104
+ loop_id: request.loop_id,
523
105
  });
524
- }
525
- if (!rounds.length)
526
- return null;
527
- // Sort by round descending, take last N
528
- rounds.sort((a, b) => b.round - a.round);
529
- const sampled = rounds.slice(0, window).reverse(); // chronological order
530
- // v1.12: Key outcomes — all rounds with output_summary
531
- const keyOutcomes = [];
532
- for (const r of sampled) {
533
- if (r.output_summary) {
534
- const status = r.success ? "✓" : "✗";
535
- keyOutcomes.push(`[R${r.round}] ${status} (${r.technique_used || "n/a"}): ` +
536
- `${String(r.output_summary).slice(0, 200)}`);
537
- }
538
- }
539
- // Recurring issues
540
- const violationCounts = new Map();
541
- for (const r of sampled) {
542
- const seen = new Set();
543
- for (const v of r.constraint_violations || []) {
544
- const vNorm = String(v).trim().toLowerCase();
545
- if (vNorm && !seen.has(vNorm)) {
546
- violationCounts.set(vNorm, (violationCounts.get(vNorm) || 0) + 1);
547
- seen.add(vNorm);
548
- }
549
- }
550
- }
551
- let recurringIssues = [...violationCounts.entries()]
552
- .filter(([, count]) => count >= 2)
553
- .sort((a, b) => b[1] - a[1])
554
- .map(([v, count]) => `${v} (appeared in ${count} rounds)`);
555
- // v1.7: Failure lineage weighting — detect and demote failure patterns
556
- const sampledRounds = sampled.map((r) => ({
557
- round: r.round,
558
- success: r.success,
559
- task: r.task,
560
- output_summary: r.output_summary,
561
- constraint_violations: r.constraint_violations,
562
- technique_used: r.technique_used,
563
- }));
564
- const failurePatterns = detectFailurePatterns(sampledRounds);
565
- const failedRoundNums = extractFailureRoundNums(failurePatterns);
566
- // Re-weight key_outcomes: outcomes from failure-pattern rounds go to end
567
- if (failedRoundNums.size > 0 && keyOutcomes.length > 0) {
568
- const promoted = [];
569
- const demoted = [];
570
- for (const ko of keyOutcomes) {
571
- const rndMatch = ko.match(/^\[R(\d+)\]/);
572
- if (rndMatch && failedRoundNums.has(parseInt(rndMatch[1], 10))) {
573
- demoted.push(ko.replace(/^\[R\d+\]/, "$& [Consider alternatives]"));
574
- }
575
- else {
576
- promoted.push(ko);
577
- }
578
- }
579
- keyOutcomes.length = 0;
580
- keyOutcomes.push(...promoted, ...demoted);
581
- }
582
- // Re-weight recurring_issues: mark issues that ONLY appear in failure-pattern rounds
583
- if (failedRoundNums.size > 0 && recurringIssues.length > 0) {
584
- // Build per-round violation sets
585
- const failureViolations = new Set();
586
- const successViolations = new Set();
587
- for (const r of sampledRounds) {
588
- const target = failedRoundNums.has(r.round) ? failureViolations : successViolations;
589
- for (const v of r.constraint_violations) {
590
- target.add(String(v).trim().toLowerCase());
591
- }
592
- }
593
- recurringIssues.length = 0;
594
- recurringIssues.push(...[...violationCounts.entries()]
595
- .filter(([, count]) => count >= 2)
596
- .sort((a, b) => b[1] - a[1])
597
- .map(([v, count]) => {
598
- const vNorm = v.trim().toLowerCase();
599
- // Mark if this issue only appears in failure rounds (possible dead end)
600
- if (failureViolations.has(vNorm) && !successViolations.has(vNorm)) {
601
- return `${v} (appeared in ${count} rounds) ⚠️ [Possible dead end — only in failed rounds]`;
602
- }
603
- return `${v} (appeared in ${count} rounds)`;
604
- }));
605
- }
606
- return makeRollingSummary({
607
- key_outcomes: keyOutcomes,
608
- recurring_issues: recurringIssues,
609
- rounds_sampled: sampled.length,
610
- generated_at_round: currentRound,
611
- failed_patterns: failurePatterns.length ? failurePatterns : [],
106
+ const last = request.last_round_result;
107
+ const history = [...(current.refinement_history ?? [])];
108
+ let objective = current.objective || request.task;
109
+ let version = current.version ?? 1;
110
+ if (last?.objective_refinement?.trim()) {
111
+ const refinement = last.objective_refinement.trim();
112
+ history.push(refinement);
113
+ objective = `${objective}\nRefinement: ${refinement}`;
114
+ version++;
115
+ }
116
+ const revisions = new Map((last?.revised_success_criteria ?? []).map((item) => [item.old, item.new]));
117
+ return makeLoopObjective({
118
+ ...current,
119
+ objective,
120
+ version,
121
+ refinement_history: history.slice(-getPolicy().evolution.max_objective_versions),
122
+ success_criteria: unique(current.success_criteria.map((item) => revisions.get(item) ?? item)),
123
+ hard_constraints: unique(current.hard_constraints),
124
+ loop_id: request.loop_id,
612
125
  });
613
126
  }
614
- export function formatRollingSummaryForPrompt(rs) {
615
- if (rs === null || rs.rounds_sampled === 0)
616
- return "";
617
- const lines = [
618
- "### Cross-Round Summary (Accumulated)",
619
- "",
620
- `**Sampled**: ${rs.rounds_sampled} prior rounds`,
621
- "",
622
- ];
623
- if (rs.key_outcomes.length) {
624
- lines.push("**Key Outcomes**:");
625
- for (const ko of rs.key_outcomes)
626
- lines.push(`- ${ko}`);
627
- lines.push("");
628
- }
629
- if (rs.recurring_issues.length) {
630
- lines.push("**Recurring Issues (appeared 2+ times)**:");
631
- for (const ri of rs.recurring_issues)
632
- lines.push(`- ⚠️ ${ri}`);
633
- lines.push("");
634
- }
635
- // v1.7: Failure patterns — explicit warnings about repeated failure paths
636
- if (rs.failed_patterns && rs.failed_patterns.length) {
637
- lines.push("### ⚠️ Failure Patterns — Consider Strategy Change");
638
- lines.push("");
639
- lines.push("The following approaches failed repeatedly. The compiler has demoted " +
640
- "their influence on key outcomes. Consider a different technique or " +
641
- "narrower scope.");
642
- for (const fp of rs.failed_patterns) {
643
- lines.push(`- 🚫 ${fp}`);
644
- }
645
- lines.push("");
646
- }
647
- return lines.join("\n");
648
- }
649
- // ═══════════════════════════════════════════════════════════════════════════
650
- // Checkpoint compression (v1.10 — subtask boundary snapshots)
651
- // ═══════════════════════════════════════════════════════════════════════════
652
- /** Build a CheckpointSummary from the current round's self-evaluation state.
653
- * Called when the Agent sets compression_checkpoint: true in its self-eval.
654
- * Snapshots the current constraints and outcome so they survive rolling-window
655
- * eviction in subsequent rounds. */
656
- export function buildCheckpointSummary(selfEval, round, activeConstraints, retiredConstraints) {
657
- const policy = getPolicy();
658
- const cpCfg = policy.checkpoint;
659
- const maxCarried = cpCfg?.max_carried_constraints ?? 10;
660
- const outcomeMaxChars = cpCfg?.outcome_max_chars ?? 200;
661
- const label = selfEval.checkpoint_label?.trim() ||
662
- `Subtask completed at round ${round}`;
663
- const outcome = (selfEval.output_summary || "").trim().slice(0, outcomeMaxChars);
664
- // Carried: constraints still active, capped
665
- const carried = activeConstraints.slice(0, maxCarried);
666
- // Resolved: constraints retired this round (retracted + the retire delta)
667
- const resolved = retiredConstraints.slice(0, maxCarried);
668
- return {
669
- label,
670
- declared_at_round: round,
671
- outcome,
672
- carried_constraints: carried,
673
- resolved_constraints: resolved,
674
- };
675
- }
676
- /** Format a CheckpointSummary as a prompt block.
677
- * Renders as a fixed section that persists across rolling-window eviction.
678
- * Returns empty string if the summary is null or has no meaningful data. */
679
- export function formatCheckpointForPrompt(cs) {
680
- if (!cs)
681
- return "";
682
- // Must have at least a label and some content to be meaningful
683
- if (!cs.label && !cs.outcome && !cs.carried_constraints.length)
684
- return "";
685
- const lines = [
686
- `### Checkpoint: ${cs.label} (Round ${cs.declared_at_round})`,
687
- "",
688
- ];
689
- if (cs.outcome) {
690
- lines.push(`**Outcome**: ${cs.outcome}`);
691
- lines.push("");
692
- }
693
- if (cs.carried_constraints.length) {
694
- lines.push("**Carried Constraints** (still active):");
695
- for (const c of cs.carried_constraints) {
696
- lines.push(`- ${c}`);
697
- }
698
- lines.push("");
699
- }
700
- if (cs.resolved_constraints.length) {
701
- lines.push("**Resolved Constraints** (closed with this checkpoint):");
702
- for (const c of cs.resolved_constraints) {
703
- lines.push(`- ${c}`);
704
- }
705
- lines.push("");
706
- }
707
- return lines.join("\n");
708
- }
709
- /** Query the vault for the most recent checkpoint entry before the current
710
- * round. Returns null if no checkpoint has been declared yet. */
711
- function loadLatestCheckpoint(loopId, currentRound, vaultContext) {
712
- if (!vaultContext)
127
+ function evolveConstraints(request, objective, previous) {
128
+ const retired = unique(request.last_round_result?.retracted_constraints ?? []);
129
+ const retiredSet = new Set(retired);
130
+ const discovered = unique(request.last_round_result?.discovered_constraints ?? [])
131
+ .slice(0, getPolicy().evolution.max_discovered_constraints_per_round);
132
+ const active = unique([
133
+ ...(previous?.constraints_active ?? []),
134
+ ...request.constraints_from_plan,
135
+ ...objective.hard_constraints,
136
+ ...objective.success_criteria,
137
+ ...discovered,
138
+ ]).filter((item) => !retiredSet.has(item))
139
+ .slice(0, getPolicy().evolution.max_active_constraints);
140
+ return { active, retired };
141
+ }
142
+ export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0) {
143
+ const window = getPolicy().summary.window;
144
+ const entries = loopEntries(loopId, context)
145
+ .filter((entry) => {
146
+ const round = entryRound(entry);
147
+ return round >= sinceRound && round < currentRound;
148
+ })
149
+ .slice(-window);
150
+ if (entries.length === 0)
713
151
  return null;
714
- const results = vaultContext.results || [];
715
- let latest = null;
716
- let latestRound = -1;
717
- for (const r of results) {
718
- const lineage = (r.loop_lineage || r.lineage || {});
719
- if (lineage.loop_id !== loopId)
720
- continue;
721
- const rnd = lineage.round;
722
- if (rnd >= currentRound)
723
- continue;
724
- // Check for stored checkpoint summary on this entry
725
- const cp = r.checkpoint_summary;
726
- if (cp && typeof cp.declared_at_round === "number" && cp.declared_at_round > latestRound) {
727
- latest = cp;
728
- latestRound = cp.declared_at_round;
729
- }
152
+ const outcomes = [];
153
+ const issues = [];
154
+ for (const entry of entries) {
155
+ const data = lineage(entry);
156
+ const round = entryRound(entry);
157
+ const success = entry.success ?? data.success;
158
+ const summary = typeof entry.output_summary === "string"
159
+ ? entry.output_summary
160
+ : typeof data.output_summary === "string" ? data.output_summary : "";
161
+ if (summary)
162
+ outcomes.push(`[R${round}] ${success === false ? "failed" : "accepted"}: ${summary}`);
163
+ const violations = Array.isArray(entry.constraint_violations)
164
+ ? entry.constraint_violations
165
+ : Array.isArray(data.constraint_violations) ? data.constraint_violations : [];
166
+ issues.push(...violations.filter((item) => typeof item === "string"));
730
167
  }
731
- if (!latest)
732
- return null;
733
- return makeCheckpointSummary({
734
- label: latest.label ?? "",
735
- declared_at_round: latest.declared_at_round ?? 0,
736
- outcome: latest.outcome ?? "",
737
- carried_constraints: latest.carried_constraints ?? [],
738
- resolved_constraints: latest.resolved_constraints ?? [],
168
+ return makeRollingSummary({
169
+ key_outcomes: unique(outcomes),
170
+ recurring_issues: unique(issues),
171
+ rounds_sampled: entries.length,
172
+ generated_at_round: currentRound,
173
+ failed_patterns: [],
739
174
  });
740
175
  }
741
- // ═══════════════════════════════════════════════════════════════════════════
742
- // External Context formatting (v1.7 — memory system integration)
743
- // ═══════════════════════════════════════════════════════════════════════════
744
- /** Format an external context string for injection into an L2 prompt.
745
- * Wraps the raw context in a marked section with a priority disclaimer.
746
- * Returns empty string if context is empty or injection is disabled by policy. */
747
- export function formatExternalContext(externalContext, sectionTitle, maxLength) {
748
- if (!externalContext || externalContext.trim().length === 0)
749
- return "";
750
- const trimmed = externalContext.trim().slice(0, maxLength);
751
- const truncationNote = trimmed.length < externalContext.trim().length
752
- ? `\n> ⚠️ Context was truncated to ${maxLength} characters.`
753
- : "";
754
- return [
755
- "",
756
- sectionTitle,
757
- "",
758
- "> ⚠️ **Role**: Situational awareness only. These are point-in-time",
759
- "> observations from past sessions — claims about code locations or",
760
- "> behaviour may be outdated. If any insight contradicts the Loop",
761
- "> Objective or Active Constraints above, the LoopForge specification",
762
- "> takes absolute precedence.",
763
- "",
764
- trimmed,
765
- truncationNote,
766
- "",
767
- "---",
768
- "",
769
- ].join("\n");
176
+ function tokens(text) {
177
+ return new Set(text.toLowerCase().match(/[a-z0-9\u4e00-\u9fff]+/g) ?? []);
770
178
  }
771
- // ═══════════════════════════════════════════════════════════════════════════
772
- // LAYER 1 — Hard Gates (can change compile level)
773
- // ═══════════════════════════════════════════════════════════════════════════
774
- export function decideLevel(request, vaultContext) {
775
- // Gate 1: Explicit override (never overrides round 1 or plan_source)
776
- if (request.force_level !== "auto" &&
777
- ["l0", "l1", "l2"].includes(request.force_level)) {
778
- if (request.round !== 1 && !request.plan_source) {
779
- return request.force_level;
780
- }
781
- }
782
- // Gate 2: First call or explicit plan input → full rebuild
783
- if (request.round === 1 || request.plan_source) {
784
- return "l2";
785
- }
786
- // Derive goal_id
787
- const goalId = deriveGoalId(request.loop_id, request.task, request.goal_id);
788
- const prev = getPreviousRound(request.loop_id, request.round - 1, vaultContext);
789
- if (prev === null)
790
- return "l2";
791
- // Gate 3: goal_id stability
792
- if (goalId !== prev.goal_id)
793
- return "l2";
794
- // Gate 4: P0–P5 cognitive evolution signals → full recompile (v1.7)
795
- // Check BEFORE the simple repair/new-constraint gates because evolution signals
796
- // (constraint discovery, objective refinement, self-correction) need full L2
797
- // processing. Only structural evolution signals trigger L2 — execution_evidence
798
- // alone does not (it is processed via the rolling summary at L1).
799
- if (request.last_round_result) {
800
- const lr = request.last_round_result;
801
- if ((lr.discovered_constraints?.length ?? 0) > 0 ||
802
- (lr.objective_refinement?.trim().length ?? 0) > 0 ||
803
- (lr.emerged_subtasks?.length ?? 0) > 0 ||
804
- (lr.retracted_constraints?.length ?? 0) > 0 ||
805
- (lr.revised_success_criteria?.length ?? 0) > 0 ||
806
- (lr.wrong_assumptions?.length ?? 0) > 0) {
807
- return "l2";
808
- }
809
- }
810
- // Gate 5: Explicit failures or new constraints → patch
811
- const hasNewConstraints = request.constraints_from_plan.length > 0;
812
- const hasNewFailures = request.last_round_result !== null &&
813
- !request.last_round_result.success;
814
- const hasRepair = detectsRepairSignal(request);
815
- if (hasNewConstraints || hasNewFailures || hasRepair)
816
- return "l1";
817
- // Nothing triggered → fast path
818
- return "l0";
179
+ function similarity(left, right) {
180
+ const a = tokens(left);
181
+ const b = tokens(right);
182
+ if (a.size === 0 || b.size === 0)
183
+ return 1;
184
+ let intersection = 0;
185
+ for (const value of a)
186
+ if (b.has(value))
187
+ intersection++;
188
+ return intersection / new Set([...a, ...b]).size;
819
189
  }
820
- // ═══════════════════════════════════════════════════════════════════════════
821
- // LAYER 2 — Soft Advisories (NEVER change compile level)
822
- // ═══════════════════════════════════════════════════════════════════════════
823
- export function alignTask(proposedTask, request, vaultContext) {
824
- const objectiveEntry = vaultGetLoopObjective(request.loop_id, vaultContext);
825
- let objData = null;
826
- if (request.loop_objective) {
827
- objData = request.loop_objective;
828
- }
829
- else if (objectiveEntry) {
830
- const lo = objectiveEntry.loop_objective;
831
- if (lo && typeof lo === "object") {
832
- objData = lo;
833
- }
834
- }
835
- if (objData === null)
190
+ export function alignTask(proposedTask, request, context) {
191
+ const objective = request.loop_objective ?? latestObjective(request.loop_id, context);
192
+ if (!objective)
836
193
  return makeTaskAlignment();
837
- const objective = objData.objective ?? "";
838
- const successCriteria = objData.success_criteria ?? [];
839
- const hardConstraints = objData.hard_constraints ?? [];
840
- const proposedTokens = tokenize(proposedTask.toLowerCase());
841
- const objText = `${objective} ${successCriteria.join(" ")} ${hardConstraints.join(" ")}`.toLowerCase();
842
- const objTokens = tokenize(objText);
843
- const score = proposedTokens.size && objTokens.size
844
- ? jaccard(proposedTokens, objTokens)
845
- : 1.0;
846
- if (score >= 0.5) {
847
- return makeTaskAlignment({
848
- is_aligned: true,
849
- alignment_score: Math.round(score * 100) / 100,
850
- });
851
- }
852
- else if (score >= 0.3) {
853
- return makeTaskAlignment({
854
- is_aligned: true,
855
- alignment_score: Math.round(score * 100) / 100,
856
- warning: `Proposed task '${proposedTask.slice(0, 80)}' may be drifting from ` +
857
- `loop objective '${objective}'. Consider narrowing scope.`,
858
- escalation: "warn",
859
- });
860
- }
861
- else {
862
- return makeTaskAlignment({
863
- is_aligned: false,
864
- alignment_score: Math.round(score * 100) / 100,
865
- warning: `Proposed task '${proposedTask.slice(0, 80)}' is OFF-OBJECTIVE. ` +
866
- `Loop objective: '${objective}'. Full realignment recommended.`,
867
- escalation: "block",
868
- });
869
- }
194
+ const score = similarity(proposedTask, [objective.objective, ...objective.success_criteria, ...objective.hard_constraints].join(" "));
195
+ return makeTaskAlignment({
196
+ is_aligned: score >= 0.3,
197
+ alignment_score: Number(score.toFixed(2)),
198
+ warning: score < 0.3 ? "Current task may be drifting from the loop objective." : "",
199
+ escalation: score < 0.3 ? "warn" : "none",
200
+ });
870
201
  }
871
- export function checkLoopHealth(loopId, request, vaultContext) {
872
- const objectiveEntry = vaultGetLoopObjective(loopId, vaultContext);
873
- let obj = null;
874
- if (request.loop_objective) {
875
- obj = request.loop_objective;
876
- }
877
- else if (objectiveEntry) {
878
- const lo = objectiveEntry.loop_objective;
879
- if (lo && typeof lo === "object") {
880
- obj = lo;
881
- }
882
- }
883
- if (obj === null)
884
- return makeLoopHealth();
885
- const objective = obj.objective ?? "";
886
- const successCriteria = obj.success_criteria ?? [];
887
- const hardConstraints = obj.hard_constraints ?? [];
888
- // 1. goal_alignment
889
- let goalAlignment = 1.0;
890
- if (request.task) {
891
- const taskTokens = tokenize(request.task.toLowerCase());
892
- const objText = `${objective} ${successCriteria.join(" ")} ${hardConstraints.join(" ")}`.toLowerCase();
893
- const objTokens = tokenize(objText);
894
- goalAlignment =
895
- taskTokens.size && objTokens.size
896
- ? jaccard(taskTokens, objTokens)
897
- : 1.0;
898
- }
899
- // 2. constraint_integrity
900
- let constraintIntegrity = 1.0;
901
- if (request.last_round_result && hardConstraints.length) {
902
- const outputText = request.last_round_result.output_summary.toLowerCase();
903
- const present = hardConstraints.filter((c) => c
904
- .toLowerCase()
905
- .split(/\s+/)
906
- .some((word) => outputText.includes(word))).length;
907
- constraintIntegrity = present / hardConstraints.length;
908
- }
909
- // 3. drift_detected
910
- const driftDetected = countConsecutiveHashMismatches(loopId, vaultContext) >= 3;
911
- // 4. strategy_stability
912
- const recent = getRecentRounds(loopId, 3, vaultContext);
913
- const strategyStability = recent.length > 0
914
- ? recent.every((r) => r.success === true)
915
- : true;
916
- // 5. task_continuity
917
- let taskContinuity = 1.0;
918
- const prevTask = getPreviousRoundTask(loopId, request.round - 1, vaultContext);
919
- if (prevTask && request.task) {
920
- const currTokens = tokenize(request.task.toLowerCase());
921
- const prevTokens = tokenize(prevTask.toLowerCase());
922
- taskContinuity =
923
- currTokens.size && prevTokens.size
924
- ? jaccard(currTokens, prevTokens)
925
- : 1.0;
926
- }
927
- // Escalation recommendation
928
- let escalation = "none";
929
- if (goalAlignment < 0.5) {
930
- escalation = "l2";
931
- }
932
- else if (constraintIntegrity < 0.7) {
933
- escalation = "l1";
934
- }
935
- else if (driftDetected) {
936
- escalation = "l2";
937
- }
202
+ export function checkLoopHealth(loopId, request, context) {
203
+ const previous = getPreviousRound(loopId, request.round - 1, context);
204
+ const alignment = alignTask(request.task, request, context);
205
+ const violations = request.last_round_result?.constraint_violations.length ?? 0;
206
+ const integrity = Math.max(0, 1 - violations * 0.2);
207
+ const continuity = previous ? similarity(previous.task, request.task) : 1;
208
+ const drift = alignment.alignment_score < 0.3 || continuity < 0.2;
938
209
  return makeLoopHealth({
939
- goal_alignment: Math.round(goalAlignment * 100) / 100,
940
- constraint_integrity: Math.round(constraintIntegrity * 100) / 100,
941
- drift_detected: driftDetected,
942
- strategy_stability: strategyStability,
943
- task_continuity: Math.round(taskContinuity * 100) / 100,
944
- escalation_recommended: escalation,
210
+ goal_alignment: alignment.alignment_score,
211
+ constraint_integrity: integrity,
212
+ drift_detected: drift,
213
+ strategy_stability: true,
214
+ task_continuity: Number(continuity.toFixed(2)),
215
+ escalation_recommended: drift || integrity < 0.6 ? "l2" : "none",
945
216
  });
946
217
  }
947
- function computeSuggestedNextTask(loopId, vaultContext) {
948
- if (vaultContext === null)
949
- return "";
950
- const results = vaultContext.results || [];
951
- let bestTask = "";
952
- let bestRound = -1;
953
- for (const r of results) {
954
- const lineage = (r.loop_lineage || r.lineage || {});
955
- if (lineage.loop_id !== loopId)
956
- continue;
957
- const rnd = lineage.round ?? 0;
958
- if (rnd > bestRound) {
959
- const task = r.task ?? r.user_intent ?? "";
960
- if (task) {
961
- bestTask = task;
962
- bestRound = rnd;
963
- }
964
- }
965
- }
966
- return bestTask ? `[R${bestRound}] Previous task: ${bestTask.slice(0, 120)}` : "";
967
- }
968
- export function computeAdvisories(request, vaultContext) {
969
- const warnings = [];
970
- let alignment = null;
971
- let health = null;
972
- let suggested = "";
973
- // goal_text_hash drift detection
974
- const currentHash = computeGoalTextHash(request.task);
975
- const prev = getPreviousRound(request.loop_id, request.round - 1, vaultContext);
976
- if (prev && currentHash !== prev.goal_text_hash && prev.goal_text_hash) {
977
- warnings.push(`goal_text_hash changed (${prev.goal_text_hash} → ${currentHash}) ` +
978
- "but goal_id matched — wording drift detected");
979
- }
980
- // Strategy collapse check
981
- if (strategyCollapse(request.loop_id, vaultContext)) {
982
- warnings.push("strategy_collapse: 3 consecutive failed rounds — " +
983
- "consider force_level=L2 rebuild");
984
- }
985
- // Repair cue detection
986
- if (detectsRepairSignal(request)) {
987
- warnings.push("repair signal detected — L1 patch applied");
988
- }
989
- // Task alignment
990
- if (request.next_task_proposal) {
991
- alignment = alignTask(request.next_task_proposal, request, vaultContext);
992
- if (alignment.escalation !== "none") {
993
- warnings.push(`task_alignment: ${alignment.warning}`);
994
- }
995
- }
996
- // Loop health
997
- const interval = Math.max(request.health_check_interval, 1);
998
- if (request.round % interval === 0) {
999
- health = checkLoopHealth(request.loop_id, request, vaultContext);
1000
- if (health.escalation_recommended !== "none") {
1001
- warnings.push(`loop_health recommends ${health.escalation_recommended}: ` +
1002
- `goal_alignment=${health.goal_alignment.toFixed(2)}, ` +
1003
- `constraint_integrity=${health.constraint_integrity.toFixed(2)}, ` +
1004
- `task_continuity=${health.task_continuity.toFixed(2)}`);
1005
- }
1006
- if (health.drift_detected) {
1007
- warnings.push("drift_detected: goal_text_hash diverged 3+ consecutive rounds");
1008
- }
1009
- }
1010
- // Forward hint
1011
- suggested = computeSuggestedNextTask(request.loop_id, vaultContext);
1012
- return { warnings, suggestedNextTask: suggested, alignment, health };
1013
- }
1014
- // ═══════════════════════════════════════════════════════════════════════════
1015
- // Plan extraction
1016
- // ═══════════════════════════════════════════════════════════════════════════
1017
- function extractObjectiveFromPlan(planPath) {
1018
- let text;
1019
- try {
1020
- text = readFileSync(planPath, "utf-8");
1021
- }
1022
- catch {
1023
- return null;
1024
- }
1025
- const sections = {
1026
- goal: [],
1027
- success: [],
1028
- constraints: [],
1029
- };
1030
- let currentSection = null;
1031
- const goalPatterns = ["goal", "objective", "目标", "目的", "意图"];
1032
- const successPatterns = [
1033
- "success criteria", "acceptance criteria", "验收标准", "成功标准",
1034
- "done when", "完成标准", "交付标准", "outcome", "deliverable",
1035
- "结果", "产出", "交付物",
1036
- ];
1037
- const constraintPatterns = [
1038
- "hard constraint", "constraint", "non-goal", "out of scope",
1039
- "硬约束", "约束", "非目标", "不做什么", "限制",
1040
- ];
1041
- for (const line of text.split("\n")) {
1042
- const stripped = line.trim();
1043
- const low = stripped
1044
- .toLowerCase()
1045
- .replace(/^#+\s*/, "");
1046
- if (stripped.startsWith("#")) {
1047
- if (goalPatterns.some((p) => low.includes(p))) {
1048
- currentSection = "goal";
1049
- continue;
1050
- }
1051
- if (successPatterns.some((p) => low.includes(p))) {
1052
- currentSection = "success";
1053
- continue;
1054
- }
1055
- if (constraintPatterns.some((p) => low.includes(p))) {
1056
- currentSection = "constraints";
1057
- continue;
1058
- }
1059
- currentSection = null;
1060
- continue;
1061
- }
1062
- if (!currentSection)
1063
- continue;
1064
- if (/^[-*•]/.test(stripped)) {
1065
- const item = stripped.replace(/^[-*•]\s*/, "").trim();
1066
- if (item && item.length > 3) {
1067
- sections[currentSection].push(item);
1068
- }
1069
- }
1070
- else if (stripped && currentSection === "goal") {
1071
- if (!sections.goal.length && stripped.length > 10) {
1072
- sections.goal.push(stripped);
1073
- }
1074
- }
1075
- else if (stripped && currentSection === "success" && stripped.length > 10) {
1076
- // P3: Also capture free-text paragraphs under success criteria headers
1077
- if (!/^#/.test(stripped)) {
1078
- sections.success.push(stripped);
1079
- }
1080
- }
1081
- }
1082
- if (!Object.values(sections).some((v) => v.length))
1083
- return null;
1084
- return {
1085
- objective: sections.goal[0] ?? "",
1086
- success_criteria: sections.success,
1087
- hard_constraints: sections.constraints,
1088
- };
1089
- }
1090
- function computeLoopObjectiveFromTask(request, _vaultContext) {
1091
- const task = request.task || "";
1092
- const constraints = [...request.constraints_from_plan];
1093
- let objective = task.trim().slice(0, 200);
1094
- const successCriteria = [];
1095
- const hardConstraints = [...constraints];
1096
- // P3: Outcome-oriented default success criteria — describe the RESULT,
1097
- // not the process. The process emerges from the loop.
1098
- if (/test|测试/i.test(task)) {
1099
- successCriteria.push("All tests pass with no regressions");
1100
- }
1101
- if (/compat|兼容/i.test(task)) {
1102
- successCriteria.push("Existing API contracts and integrations unchanged");
1103
- }
1104
- if (/security|安全|audit/i.test(task)) {
1105
- successCriteria.push("All identified vulnerabilities resolved or documented");
1106
- }
1107
- if (/fix|修复|bug/i.test(task)) {
1108
- successCriteria.push("Root cause addressed and verified, not just symptoms");
1109
- }
1110
- if (/build|create|implement|开发|实现|构建/i.test(task)) {
1111
- successCriteria.push("Deliverable matches specification and passes review");
1112
- }
1113
- if (/refactor|重构/i.test(task)) {
1114
- successCriteria.push("Behavior preserved, structure improved, no new failures");
1115
- }
1116
- if (/migrate|迁移|upgrade|升级/i.test(task)) {
1117
- successCriteria.push("Migration completes without data loss or downtime");
1118
- }
1119
- if (/perf|performance|optimize|性能|优化/i.test(task)) {
1120
- successCriteria.push("Measurable improvement verified by benchmarks");
1121
- }
1122
- // P3: Try to extract richer objective from plan_source
1123
- if (request.plan_source) {
1124
- const planExtracted = extractObjectiveFromPlan(request.plan_source);
1125
- if (planExtracted) {
1126
- if (planExtracted.objective) {
1127
- objective = planExtracted.objective;
1128
- }
1129
- if (planExtracted.success_criteria) {
1130
- successCriteria.push(...planExtracted.success_criteria);
1131
- }
1132
- if (planExtracted.hard_constraints) {
1133
- hardConstraints.push(...planExtracted.hard_constraints);
1134
- }
1135
- }
1136
- else {
1137
- hardConstraints.push(`Follow plan: ${request.plan_source}`);
1138
- }
1139
- }
1140
- // P3: Derive domain from task content for context-aware defaults
1141
- if (!successCriteria.length) {
1142
- if (/\.sol|solidity|contract|智能合约/i.test(task)) {
1143
- successCriteria.push("All functions respect check-effects-interactions pattern");
1144
- successCriteria.push("No unchecked external calls or reentrancy vectors");
1145
- }
1146
- else if (/\.tsx?|\.jsx?|react|component|组件/i.test(task)) {
1147
- successCriteria.push("Component renders correctly across all states (loading, empty, error)");
1148
- successCriteria.push("No prop-type or accessibility regressions");
1149
- }
1150
- else if (/api|endpoint|接口|route/i.test(task)) {
1151
- successCriteria.push("Endpoint responds correctly for all defined status codes");
1152
- successCriteria.push("Error responses follow the project's error schema");
1153
- }
1154
- else {
1155
- successCriteria.push("Task goal achieved with verifiable evidence");
1156
- }
1157
- }
1158
- if (!hardConstraints.length) {
1159
- hardConstraints.push("Do not modify files outside the stated scope");
1160
- hardConstraints.push("Preserve existing test coverage — no test deletions without replacement");
1161
- }
1162
- return makeLoopObjective({
1163
- objective,
1164
- success_criteria: successCriteria,
1165
- hard_constraints: hardConstraints,
1166
- created_at_round: 1,
1167
- loop_id: request.loop_id,
1168
- version: 1,
1169
- refinement_history: [],
218
+ function levelDecision(request, context) {
219
+ const previous = getPreviousRound(request.loop_id, request.round - 1, context);
220
+ const last = request.last_round_result;
221
+ const hasNewInformation = Boolean(request.new_since_last_round.trim() ||
222
+ last?.discovered_constraints?.length ||
223
+ last?.objective_refinement?.trim() ||
224
+ last?.emerged_subtasks?.length ||
225
+ last?.retracted_constraints?.length ||
226
+ last?.revised_success_criteria?.length ||
227
+ last?.wrong_assumptions?.length);
228
+ const lastFullRound = loopEntries(request.loop_id, context)
229
+ .filter((entry) => lineage(entry).recompile_level === "l2")
230
+ .map(entryRound)
231
+ .filter((round) => round < request.round)
232
+ .at(-1) ?? 1;
233
+ return decidePromptLevel({
234
+ round: request.round,
235
+ attempt: request.attempt,
236
+ forceLevel: request.force_level,
237
+ hasPlanSource: Boolean(request.plan_source),
238
+ checkpointBoundary: last?.compression_checkpoint === true,
239
+ goalChanged: previous !== null && previous.goal_id !== deriveGoalId(request.loop_id, request.task, request.goal_id),
240
+ previousStateMissing: request.round > 1 && previous === null,
241
+ previousFailedWithoutNewInformation: last?.success === false && !hasNewInformation,
242
+ verificationContradicted: (request.verification_flags ?? [])
243
+ .some((flag) => flag.severity === "error"),
244
+ consecutiveRejections: request.consecutive_rejections,
245
+ fullRefreshInterval: getPolicy().prompt.full_refresh_interval,
246
+ lastFullRound,
1170
247
  });
1171
248
  }
1172
- // ═══════════════════════════════════════════════════════════════════════════
1173
- // Compilation — L0 / L1 / L2
1174
- // ═══════════════════════════════════════════════════════════════════════════
1175
- function compileL0(request, vaultContext, prevRound) {
1176
- const prev = prevRound ??
1177
- getPreviousRound(request.loop_id, request.round - 1, vaultContext);
1178
- const cachedPrompt = prev?.prompt_text ?? "";
1179
- if (cachedPrompt) {
1180
- return makeLoopCompileResponse({
1181
- status: AgentStatus.OK,
1182
- prompt: cachedPrompt,
1183
- recompile_level: "l0",
1184
- diff_from_previous: `L0 cache hit — reusing prompt from round ${request.round - 1}`,
1185
- lineage: [`${request.loop_id}:r${request.round}`],
1186
- constraints_active: prev?.constraints_active ?? [],
1187
- constraints_retired: [],
1188
- technique_used: "cached",
1189
- loop_id: request.loop_id,
1190
- round: request.round,
1191
- goal_id: deriveGoalId(request.loop_id, request.task, request.goal_id),
1192
- goal_text_hash: computeGoalTextHash(request.task),
1193
- plan_source: request.plan_source,
1194
- warnings: [],
1195
- });
1196
- }
1197
- // Auto-escalate: no cached prompt → delegate to L2
1198
- const l2Response = compileL2(request, vaultContext);
1199
- l2Response.recompile_level = "l0";
1200
- l2Response.diff_from_previous =
1201
- `L0 auto-escalated to L2 — no cached prompt available from round ${request.round - 1}`;
1202
- return l2Response;
249
+ export function decideLevel(request, context) {
250
+ return levelDecision(request, context).level;
1203
251
  }
1204
- function compileL1(request, vaultContext, prevRound) {
1205
- const prev = prevRound ??
1206
- getPreviousRound(request.loop_id, request.round - 1, vaultContext);
1207
- const goalId = deriveGoalId(request.loop_id, request.task, request.goal_id);
1208
- const newConstraints = [...request.constraints_from_plan];
1209
- let activeRaw = [...(prev?.constraints_active ?? []), ...newConstraints];
1210
- // Deduplicate preserving order
1211
- activeRaw = [...new Set(activeRaw)];
1212
- // v3.5: Constraint retirement
1213
- const { active, retired } = computeConstraintRetirement(activeRaw, request.loop_id, request.round, vaultContext);
1214
- // v3.5: Rolling summary
1215
- const rollingSummary = buildRollingSummary(request.loop_id, request.round, vaultContext);
1216
- const rollingText = formatRollingSummaryForPrompt(rollingSummary);
1217
- const violations = request.last_round_result?.constraint_violations ?? [];
1218
- const diffParts = [];
1219
- if (newConstraints.length) {
1220
- diffParts.push(`new constraints: [${newConstraints.join(", ")}]`);
1221
- }
1222
- if (retired.length) {
1223
- diffParts.push(`retired constraints: [${retired.join(", ")}]`);
1224
- }
1225
- if (violations.length) {
1226
- diffParts.push(`violations from last round: [${violations.join(", ")}]`);
1227
- }
1228
- if (request.new_since_last_round) {
1229
- diffParts.push(`delta: ${request.new_since_last_round.slice(0, 200)}`);
1230
- }
1231
- // Build patched prompt
1232
- const lines = [
1233
- `## Loop Round ${request.round} — L1 Patch`,
252
+ export function buildSelfEvalBlock(round) {
253
+ return [
254
+ "### LoopForge Evaluation (Required)",
1234
255
  "",
1235
- `**Goal**: ${request.task}`,
1236
- `**Loop ID**: ${request.loop_id}`,
1237
- `**Goal ID**: ${goalId}`,
256
+ `After completing Round ${round}, submit JSON between these markers:`,
257
+ "---loopforge-eval",
258
+ JSON.stringify({
259
+ success: false,
260
+ output_summary: `<verified result of round ${round}>`,
261
+ constraint_violations: [],
262
+ should_continue: true,
263
+ discovered_constraints: [],
264
+ emerged_subtasks: [],
265
+ execution_evidence: {
266
+ files_changed: [],
267
+ test_results: { passed: 0, failed: 0, skipped: 0 },
268
+ success_criteria_met: [],
269
+ success_criteria_remaining: [],
270
+ progress_estimate: 0,
271
+ },
272
+ wrong_assumptions: [],
273
+ next_action: "<next concrete action, or empty when complete>",
274
+ }, null, 2),
275
+ "---end-loopforge-eval",
1238
276
  "",
1239
- ];
1240
- if (rollingText) {
1241
- lines.push(rollingText);
1242
- lines.push("");
1243
- }
1244
- if (active.length) {
1245
- lines.push("### Active Constraints (inherited + new, pruned)");
1246
- for (const c of active)
1247
- lines.push(`- ${c}`);
1248
- lines.push("");
1249
- }
1250
- if (retired.length) {
1251
- lines.push("### Retired Constraints (no recent activity)");
1252
- for (const c of retired)
1253
- lines.push(`- ~${c}~`);
1254
- lines.push("");
1255
- }
1256
- if (violations.length) {
1257
- lines.push("### Violations From Last Round (must fix)");
1258
- for (const v of violations)
1259
- lines.push(`- ${v}`);
1260
- lines.push("");
1261
- }
1262
- if (request.new_since_last_round) {
1263
- lines.push("### What Changed Since Last Round");
1264
- lines.push(request.new_since_last_round);
1265
- lines.push("");
1266
- }
1267
- if (request.last_round_result?.output_summary) {
1268
- lines.push("### Last Round Summary");
1269
- lines.push(request.last_round_result.output_summary);
1270
- lines.push("");
1271
- }
1272
- lines.push("### Task");
1273
- lines.push(request.task);
1274
- return makeLoopCompileResponse({
1275
- status: AgentStatus.OK,
1276
- prompt: lines.join("\n"),
1277
- recompile_level: "l1",
1278
- diff_from_previous: diffParts.length ? diffParts.join("; ") : "Patch applied.",
1279
- lineage: [`${request.loop_id}:r${request.round}`],
1280
- constraints_active: active,
1281
- constraints_retired: retired,
1282
- technique_used: "patch",
1283
- rolling_summary: rollingSummary,
1284
- loop_id: request.loop_id,
1285
- round: request.round,
1286
- goal_id: goalId,
1287
- goal_text_hash: computeGoalTextHash(request.task),
1288
- plan_source: request.plan_source,
1289
- warnings: [],
277
+ "Set success=true only when the full goal and hard constraints are verified.",
278
+ ].join("\n");
279
+ }
280
+ function checkpoint(request, active, retired) {
281
+ const last = request.last_round_result;
282
+ if (!last?.compression_checkpoint)
283
+ return null;
284
+ const policy = getPolicy().checkpoint;
285
+ return makeCheckpointSummary({
286
+ label: last.checkpoint_label?.trim() || `Round ${last.round}`,
287
+ declared_at_round: last.round,
288
+ outcome: last.output_summary.slice(0, policy.outcome_max_chars),
289
+ carried_constraints: active.slice(0, policy.max_carried_constraints),
290
+ resolved_constraints: retired,
1290
291
  });
1291
292
  }
1292
- export function compileL2(request, vaultContext) {
1293
- const goalId = deriveGoalId(request.loop_id, request.task, request.goal_id);
1294
- // v4.0: Tier-gated technique routing
1295
- const isCheckpoint = request.last_round_result?.compression_checkpoint === true;
1296
- const analysis = routeTechniqueAdaptive(request.task, vaultContext, request.loop_id, isCheckpoint);
1297
- const technique = analysis.technique;
1298
- const referenceFile = analysis.reference_file;
1299
- // v1.10: Checkpoint — load existing or build new from last round's self-eval
1300
- let checkpointSummary = null;
1301
- let checkpointRound = 0;
1302
- // Check if the last round declared a checkpoint
1303
- const lr = request.last_round_result;
1304
- if (lr?.compression_checkpoint === true) {
1305
- // Build fresh checkpoint from the last round's data
1306
- // Constraints haven't been updated yet at this point, but we snapshot the
1307
- // current state as seen by this compile call (pre-P5 correction).
1308
- // We defer building until constraints are finalized below.
1309
- checkpointRound = request.round - 1;
1310
- }
1311
- else if (request.round > 1) {
1312
- // No new checkpoint — try loading the previous one from vault
1313
- checkpointSummary = loadLatestCheckpoint(request.loop_id, request.round, vaultContext);
1314
- if (checkpointSummary) {
1315
- checkpointRound = checkpointSummary.declared_at_round;
1316
- }
1317
- }
1318
- // v3.5: Rolling summary — optionally scoped to post-checkpoint rounds
1319
- const rollingSummary = buildRollingSummary(request.loop_id, request.round, vaultContext, checkpointRound > 0 ? checkpointRound : undefined);
1320
- // P5c: Wrong assumptions — must be applied BEFORE formatting so they appear
1321
- // in the rendered cross-round summary text injected into the prompt.
1322
- if (lr?.wrong_assumptions?.length) {
1323
- if (rollingSummary) {
1324
- rollingSummary.key_outcomes = [
1325
- ...rollingSummary.key_outcomes,
1326
- ...lr.wrong_assumptions.map((a) => `Wrong assumption corrected: ${a}`),
1327
- ];
1328
- }
1329
- }
1330
- const rollingText = formatRollingSummaryForPrompt(rollingSummary);
1331
- // ── Loop Objective: create at round 1, load from vault at later rounds ──
1332
- let loopObjective = null;
1333
- if (request.round === 1) {
1334
- if (request.loop_objective) {
1335
- loopObjective = request.loop_objective;
1336
- }
1337
- else {
1338
- loopObjective = computeLoopObjectiveFromTask(request, vaultContext);
1339
- }
1340
- }
1341
- else {
1342
- // P1 fix: Load loop objective from vault for round > 1
1343
- const vaultLo = vaultGetLoopObjective(request.loop_id, vaultContext);
1344
- if (vaultLo) {
1345
- const loData = vaultLo.loop_objective;
1346
- if (loData) {
1347
- loopObjective = makeLoopObjective({
1348
- objective: loData.objective ?? "",
1349
- success_criteria: loData.success_criteria ?? [],
1350
- hard_constraints: loData.hard_constraints ?? [],
1351
- created_at_round: loData.created_at_round ?? 1,
1352
- loop_id: loData.loop_id ?? request.loop_id,
1353
- version: loData.version ?? 1,
1354
- refinement_history: loData.refinement_history ?? [],
1355
- });
1356
- }
1357
- }
1358
- if (!loopObjective && request.loop_objective) {
1359
- loopObjective = request.loop_objective;
1360
- }
1361
- }
1362
- // ── P1: Apply objective_refinement from last round ──
1363
- if (loopObjective && request.last_round_result?.objective_refinement) {
1364
- const refinement = request.last_round_result.objective_refinement.trim();
1365
- if (refinement) {
1366
- const version = (loopObjective.version ?? 1) + 1;
1367
- const history = [...(loopObjective.refinement_history ?? []), refinement];
1368
- // Enforce max versions from policy
1369
- const policy = getPolicy();
1370
- const maxVersions = policy.evolution?.max_objective_versions ?? 10;
1371
- const prunedHistory = history.slice(-maxVersions);
1372
- const refinedObjective = `${loopObjective.objective} [R${request.round}: ${refinement}]`;
1373
- loopObjective = makeLoopObjective({
1374
- ...loopObjective,
1375
- objective: refinedObjective,
1376
- version,
1377
- refinement_history: prunedHistory,
1378
- });
1379
- }
1380
- }
1381
- // ── Build active constraints (base + LO + P0 discovered) ──
1382
- let constraints = [...request.constraints_from_plan];
1383
- if (loopObjective) {
1384
- constraints = [
1385
- ...new Set([
1386
- ...constraints,
1387
- ...loopObjective.hard_constraints,
1388
- ...loopObjective.success_criteria,
1389
- ]),
1390
- ];
1391
- }
1392
- // P0: Merge discovered constraints from last round
1393
- if (request.last_round_result?.discovered_constraints?.length) {
1394
- const policy = getPolicy();
1395
- const maxPerRound = policy.evolution?.max_discovered_constraints_per_round ?? 5;
1396
- const maxActive = policy.evolution?.max_active_constraints ?? 15;
1397
- const newConstraints = request.last_round_result.discovered_constraints
1398
- .slice(0, maxPerRound)
1399
- .filter((c) => !constraints.includes(c));
1400
- constraints = [...constraints, ...newConstraints].slice(0, maxActive);
1401
- }
1402
- // ═══════════════════════════════════════════════════════════════════════════
1403
- // P5: Self-correction — MUST process retractions/revisions BEFORE prompt build
1404
- // so the technician compiler sees the corrected constraints and loop objective.
1405
- // ═══════════════════════════════════════════════════════════════════════════
1406
- const warnings = [];
1407
- const retired = [];
1408
- // P5a: Retracted constraints — remove from active set
1409
- if (lr?.retracted_constraints?.length) {
1410
- for (const rc of lr.retracted_constraints) {
1411
- const idx = constraints.indexOf(rc);
1412
- if (idx >= 0) {
1413
- constraints.splice(idx, 1);
1414
- retired.push(`[agent-retracted] ${rc}`);
1415
- }
1416
- }
1417
- }
1418
- // P5b: Revised success criteria — update Loop Objective
1419
- if (lr?.revised_success_criteria?.length && loopObjective) {
1420
- const sc = [...loopObjective.success_criteria];
1421
- for (const rev of lr.revised_success_criteria) {
1422
- const idx = sc.indexOf(rev.old);
1423
- if (idx >= 0) {
1424
- sc[idx] = rev.new;
1425
- }
1426
- else {
1427
- sc.push(rev.new);
1428
- }
1429
- }
1430
- const version = (loopObjective.version ?? 1) + 1;
1431
- const history = [
1432
- ...(loopObjective.refinement_history ?? []),
1433
- `R${request.round}: revised ${lr.revised_success_criteria.length} success criteria`,
1434
- ];
1435
- const updatedLo = makeLoopObjective({
1436
- ...loopObjective,
1437
- success_criteria: sc,
1438
- version,
1439
- refinement_history: history,
1440
- });
1441
- // Rebuild constraints to include revised criteria
1442
- constraints = [...new Set([
1443
- ...constraints.filter((c) => !updatedLo.hard_constraints.includes(c) && !updatedLo.success_criteria.includes(c)),
1444
- ...updatedLo.hard_constraints,
1445
- ...updatedLo.success_criteria,
1446
- ])];
1447
- loopObjective = updatedLo;
1448
- }
1449
- // ── v1.10: Build checkpoint summary if last round declared one ──
1450
- if (checkpointRound > 0 && !checkpointSummary && lr) {
1451
- // checkpointRound was set but no summary yet → fresh declaration
1452
- // We need the self-eval from last round. It's embedded in lr fields.
1453
- // Build a synthetic SelfEvaluation from the last_round_result for the builder.
1454
- const synthEval = {
1455
- success: lr.success,
1456
- output_summary: lr.output_summary,
1457
- constraint_violations: lr.constraint_violations,
1458
- should_continue: true,
1459
- compression_checkpoint: true,
1460
- checkpoint_label: lr.checkpoint_label ?? "",
1461
- };
1462
- checkpointSummary = buildCheckpointSummary(synthEval, checkpointRound, constraints, retired);
1463
- }
1464
- // ── Route to technique-specific specialist (v1.1 deep integration) ──
1465
- let prompt;
1466
- if (technique === "step-back") {
1467
- prompt = compileStepBack(request, goalId, constraints, loopObjective, rollingText, analysis);
1468
- }
1469
- else if (technique === "least-to-most") {
1470
- prompt = compileLeastToMost(request, goalId, constraints, loopObjective, rollingText, analysis);
1471
- }
1472
- else if (technique === "tree-of-thought") {
1473
- prompt = compileToT(request, goalId, constraints, loopObjective, rollingText, analysis);
1474
- }
1475
- else {
1476
- prompt = compileGeneric(request, goalId, constraints, loopObjective, rollingText, analysis, referenceFile, technique);
1477
- }
1478
- // ── v1.10: Inject checkpoint block (before rolling summary in prompt) ──
1479
- // The checkpoint is rendered before the external context so it appears
1480
- // in a natural position: after Loop Objective, before Cross-Round Summary.
1481
- if (checkpointSummary) {
1482
- prompt += "\n" + formatCheckpointForPrompt(checkpointSummary);
1483
- }
1484
- // ── v1.7: Inject external memory context (L2 only) ──
1485
- const policy = getPolicy();
1486
- if (policy.memory_injection.enabled && request.external_context) {
1487
- prompt += formatExternalContext(request.external_context, policy.memory_injection.section_title, policy.memory_injection.max_context_length);
1488
- }
1489
- // ── P2: Emerged subtasks → suggested next task ──
1490
- let suggestedNextTask = "";
1491
- if (request.last_round_result?.emerged_subtasks?.length) {
1492
- suggestedNextTask = request.last_round_result.emerged_subtasks
1493
- .slice(0, 3)
1494
- .join("; ");
1495
- }
1496
- // ═══════════════════════════════════════════════════════════════════════════
1497
- // P4: Consistency validation — cross-check agent claims
1498
- // ═══════════════════════════════════════════════════════════════════════════
1499
- const evidence = lr?.execution_evidence;
1500
- if (evidence) {
1501
- // Check 1: Agent claims action but reports no files changed
1502
- if (evidence.files_changed.length === 0 && lr.output_summary) {
1503
- const actionWords = /\b(fix|modif|chang|updat|refactor|implement|add|remov|delet|rewrit|修补|修改|更改|更新|重构|实现|添加|删除|重写)\b/i;
1504
- if (actionWords.test(lr.output_summary)) {
1505
- warnings.push("P4: Agent output claims changes but execution_evidence.files_changed is empty — possible oversight");
1506
- }
1507
- }
1508
- // Check 2: Agent reports success but tests failed
1509
- if (evidence.test_results && evidence.test_results.failed > 0 && lr.success) {
1510
- warnings.push(`P4: Agent reports success=true but ${evidence.test_results.failed} tests failed — claims may not match reality`);
1511
- }
1512
- // Check 3: Progress estimate mismatch with criteria tracking
1513
- if (loopObjective && loopObjective.success_criteria.length > 0) {
1514
- const metCount = evidence.success_criteria_met.length;
1515
- const totalCount = loopObjective.success_criteria.length;
1516
- const objectiveProgress = totalCount > 0 ? metCount / totalCount : 0;
1517
- const subjectiveProgress = evidence.progress_estimate;
1518
- const policy = getPolicy();
1519
- const mismatchThreshold = policy.evolution?.progress_mismatch_threshold ?? 0.3;
1520
- if (Math.abs(objectiveProgress - subjectiveProgress) > mismatchThreshold) {
1521
- warnings.push(`P4: Progress estimate mismatch — agent estimates ${(subjectiveProgress * 100).toFixed(0)}% but ${metCount}/${totalCount} criteria met (${(objectiveProgress * 100).toFixed(0)}%)`);
1522
- }
1523
- }
1524
- }
1525
- // ═══════════════════════════════════════════════════════════════════════════
1526
- // P4: Progress gradient — early stall detection
1527
- // ═══════════════════════════════════════════════════════════════════════════
1528
- if (evidence && vaultContext) {
1529
- const prevEvidence = getPreviousRoundEvidence(request.loop_id, request.round - 1, vaultContext);
1530
- if (prevEvidence) {
1531
- const delta = evidence.progress_estimate - prevEvidence.progress_estimate;
1532
- const policy = getPolicy();
1533
- const stallThreshold = policy.evolution?.progress_stall_threshold ?? 0.05;
1534
- const stallRounds = policy.evolution?.progress_stall_rounds ?? 2;
1535
- if (delta < stallThreshold && evidence.progress_estimate < 0.95) {
1536
- // Check how many consecutive rounds have been below threshold
1537
- const stallCount = countProgressStallRounds(request.loop_id, request.round, vaultContext, stallThreshold);
1538
- if (stallCount >= stallRounds) {
1539
- warnings.push(`P4: Progress has stalled — <${(stallThreshold * 100).toFixed(0)}% improvement for ${stallCount} consecutive rounds. Current: ${(evidence.progress_estimate * 100).toFixed(0)}%`);
1540
- }
1541
- }
1542
- }
1543
- }
1544
- // ═══════════════════════════════════════════════════════════════════════════
1545
- // P4: Build Progress Dashboard for prompt injection
1546
- // ═══════════════════════════════════════════════════════════════════════════
1547
- let progressDashboard = "";
1548
- if (evidence && loopObjective) {
1549
- const totalCriteria = loopObjective.success_criteria.length;
1550
- const metCriteria = evidence.success_criteria_met.length;
1551
- const remainingCriteria = evidence.success_criteria_remaining;
1552
- const objProgress = totalCriteria > 0 ? metCriteria / totalCriteria : evidence.progress_estimate;
1553
- const subjProgress = evidence.progress_estimate;
1554
- const lines = [
1555
- "",
1556
- "### Progress Dashboard",
1557
- "",
1558
- `**Overall**: ${metCriteria}/${totalCriteria} criteria met (${(objProgress * 100).toFixed(0)}%)`,
1559
- ];
1560
- if (Math.abs(objProgress - subjProgress) > 0.05) {
1561
- lines.push(`**Agent estimate**: ${(subjProgress * 100).toFixed(0)}%`);
1562
- }
1563
- if (evidence.files_changed.length) {
1564
- lines.push(`**Files changed**: ${evidence.files_changed.join(", ")}`);
1565
- }
1566
- if (evidence.test_results) {
1567
- const tr = evidence.test_results;
1568
- lines.push(`**Tests**: ${tr.passed} passed, ${tr.failed} failed, ${tr.skipped} skipped`);
1569
- }
1570
- if (remainingCriteria.length) {
1571
- lines.push("**Remaining criteria**:");
1572
- for (const rc of remainingCriteria.slice(0, 5)) {
1573
- lines.push(`- ${rc}`);
1574
- }
1575
- if (remainingCriteria.length > 5) {
1576
- lines.push(`- ... and ${remainingCriteria.length - 5} more`);
1577
- }
1578
- }
1579
- lines.push("");
1580
- // Compute trend from previous round
1581
- if (vaultContext) {
1582
- const prevEvidence = getPreviousRoundEvidence(request.loop_id, request.round - 1, vaultContext);
1583
- if (prevEvidence) {
1584
- const trend = objProgress - (prevEvidence.success_criteria_met.length / Math.max(totalCriteria, 1));
1585
- if (trend > 0.03) {
1586
- lines.push(`**Trend**: ↑ advancing (+${(trend * 100).toFixed(0)}% this round)`);
1587
- }
1588
- else if (trend > -0.03) {
1589
- lines.push("**Trend**: → stable");
1590
- }
1591
- else {
1592
- lines.push(`**Trend**: ↓ regressing (${(trend * 100).toFixed(0)}% this round)`);
1593
- }
1594
- lines.push("");
1595
- }
1596
- }
1597
- progressDashboard = lines.join("\n");
1598
- }
1599
- return makeLoopCompileResponse({
293
+ export function compileLoop(request, context) {
294
+ const decision = levelDecision(request, context);
295
+ const previous = getPreviousRound(request.loop_id, request.round - 1, context);
296
+ const objective = evolveObjective(request, context);
297
+ const constraints = evolveConstraints(request, objective, previous);
298
+ const rolling = buildRollingSummary(request.loop_id, request.round, context);
299
+ const alignment = alignTask(request.task, request, context);
300
+ const health = checkLoopHealth(request.loop_id, { ...request, loop_objective: objective }, context);
301
+ const warnings = unique([
302
+ alignment.warning,
303
+ health.escalation_recommended !== "none"
304
+ ? `Loop health recommends ${health.escalation_recommended}.`
305
+ : "",
306
+ ]);
307
+ const response = makeLoopCompileResponse({
1600
308
  status: AgentStatus.OK,
1601
- prompt: prompt + progressDashboard,
1602
- recompile_level: "l2",
1603
- diff_from_previous: request.round === 1 || request.plan_source
1604
- ? "Full recompile — new goal or first call."
1605
- : "Full recompile — goal_id changed or strategy collapse.",
309
+ recompile_level: decision.level,
310
+ diff_from_previous: decision.reasons.join(","),
1606
311
  lineage: [`${request.loop_id}:r${request.round}`],
1607
- constraints_active: constraints,
1608
- constraints_retired: retired,
1609
- technique_used: technique,
1610
- reference_file: referenceFile,
1611
- rolling_summary: rollingSummary,
1612
- checkpoint_summary: checkpointSummary,
312
+ constraints_active: constraints.active,
313
+ constraints_retired: constraints.retired,
1613
314
  loop_id: request.loop_id,
1614
315
  round: request.round,
1615
- goal_id: goalId,
316
+ goal_id: deriveGoalId(request.loop_id, request.task, request.goal_id),
1616
317
  goal_text_hash: computeGoalTextHash(request.task),
1617
- loop_objective: loopObjective,
318
+ loop_objective: objective,
319
+ loop_health: health,
320
+ task_alignment: alignment,
321
+ rolling_summary: rolling,
322
+ checkpoint_summary: checkpoint(request, constraints.active, constraints.retired),
323
+ suggested_next_task: request.last_round_result?.next_action?.trim() ||
324
+ request.last_round_result?.emerged_subtasks?.[0] || "",
1618
325
  plan_source: request.plan_source,
1619
- suggested_next_task: suggestedNextTask,
1620
326
  warnings,
1621
327
  });
1622
- }
1623
- function buildHeader(ctx, technique) {
1624
- const lines = [
1625
- `## LoopForge L2 Compile — Round ${ctx.request.round}`,
1626
- `**Technique**: ${technique} (embedded)`,
1627
- "",
1628
- ];
1629
- if (ctx.rollingText) {
1630
- lines.push(ctx.rollingText);
1631
- lines.push("");
1632
- }
1633
- return lines;
1634
- }
1635
- function buildLoopObjectiveBlock(lo) {
1636
- if (!lo)
1637
- return [];
1638
- const lines = [
1639
- "### Loop Objective (Anchor)",
1640
- `**Objective**: ${lo.objective}`,
1641
- ];
1642
- if (lo.success_criteria.length) {
1643
- lines.push("**Success Criteria**:");
1644
- for (const sc of lo.success_criteria)
1645
- lines.push(`- ${sc}`);
1646
- }
1647
- if (lo.hard_constraints.length) {
1648
- lines.push("**Hard Constraints**:");
1649
- for (const hc of lo.hard_constraints)
1650
- lines.push(`- ${hc}`);
1651
- }
1652
- lines.push("");
1653
- return lines;
1654
- }
1655
- function buildConstraintsBlock(constraints) {
1656
- if (!constraints.length)
1657
- return [];
1658
- const lines = ["### Active Constraints"];
1659
- for (const c of constraints)
1660
- lines.push(`- ${c}`);
1661
- lines.push("");
1662
- return lines;
1663
- }
1664
- function buildTaskBlock(ctx) {
1665
- const lines = [
1666
- "### Task",
1667
- ctx.request.task,
1668
- "",
1669
- ];
1670
- if (ctx.request.domain) {
1671
- lines.push(`**Domain**: ${ctx.request.domain}`);
1672
- lines.push("");
1673
- }
1674
- return lines;
1675
- }
1676
- function buildIdentityBlock(ctx) {
1677
- return [
1678
- "### Loop Identity",
1679
- `- Loop ID: \`${ctx.request.loop_id}\``,
1680
- `- Goal ID: \`${ctx.goalId}\``,
1681
- `- Round: ${ctx.request.round}`,
1682
- "",
1683
- ];
1684
- }
1685
- /** Generic compiler — reads technique reference file (fallback for zero-shot/few-shot/cot). */
1686
- function compileGeneric(request, goalId, constraints, loopObjective, rollingText, analysis, referenceFile, technique) {
1687
- const ctx = { request, goalId, constraints, loopObjective, rollingText };
1688
- const lines = [
1689
- ...buildHeader(ctx, technique),
1690
- `Read the technique reference BEFORE generating the prompt:`,
1691
- ` Technique: ${technique}`,
1692
- ` Reference: ${referenceFile}`,
1693
- ` Rationale: ${analysis.rationale}`,
1694
- "",
1695
- ...buildLoopObjectiveBlock(loopObjective),
1696
- ...buildConstraintsBlock(constraints),
1697
- ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []),
1698
- ...buildTaskBlock(ctx),
1699
- ...buildIdentityBlock(ctx),
1700
- "### Generation Instructions",
1701
- `1. Read \`${referenceFile}\` — study its structure rules, section count, and format requirements`,
1702
- "2. Generate a complete prompt following that technique's structure",
1703
- "3. The prompt must be self-contained — ready for a coding agent to execute",
1704
- "4. Output only the generated prompt — no preamble, no meta-commentary",
1705
- ];
1706
- return lines.join("\n");
1707
- }
1708
- // ── Step-Back specialist ───────────────────────────────────────────────────────
1709
- function compileStepBack(request, goalId, constraints, loopObjective, rollingText, analysis) {
1710
- const ctx = { request, goalId, constraints, loopObjective, rollingText };
1711
- const lines = [
1712
- ...buildHeader(ctx, "step-back"),
1713
- `**Rationale**: ${analysis.rationale}`,
1714
- "",
1715
- "Generate a complete prompt using the **Step-Back** technique. Follow the 8-section skeleton below.",
1716
- "Embed all structural constraints — do NOT reference an external file.",
1717
- "",
1718
- ...buildLoopObjectiveBlock(loopObjective),
1719
- ...buildConstraintsBlock(constraints),
1720
- ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []),
1721
- ...buildTaskBlock(ctx),
1722
- ...buildIdentityBlock(ctx),
1723
- "",
1724
- "### 8-Section Skeleton (REQUIRED)",
1725
- "",
1726
- "| # | Section | Required | Notes |",
1727
- "|---|---------|----------|-------|",
1728
- "| 1 | 角色 | ✓ | Domain expert role appropriate to the task |",
1729
- "| 2 | 任务 | ✓ | One-sentence task summary |",
1730
- "| 3 | 输入 | ✓ | Target module/problem to analyse |",
1731
- "| 4 | 输出格式 | ✓ | Numbered list of expected outputs |",
1732
- "| 5 | **Step-Back 抽象框架** | ✓ | See format rules below |",
1733
- "| 6 | 具体实现要求 | ✓ | MUST start with: **\"基于上述抽象框架,实现以下所有功能\"** |",
1734
- "| 7 | 硬约束 | ✓ | Numbered list of hard constraints |",
1735
- "| 8 | 生成要求 | ✓ | Acceptance criteria |",
1736
- "",
1737
- "### Section 5 — Step-Back 抽象框架 (CRITICAL)",
1738
- "",
1739
- "Must contain **2-3 abstract frameworks**. Each framework is an independent ASCII diagram.",
1740
- "Frameworks are PARALLEL (peer-level), not sequential.",
1741
- "",
1742
- "Each framework format:",
1743
- "```",
1744
- "### 框架N:[Framework Name]",
1745
- "",
1746
- "[ASCII diagram with principles/formulas/classification tables/rule tables]",
1747
- "",
1748
- "Diagram content requirements:",
1749
- "- Core principle/formula (e.g. \"Total = Σ(dimension × weight)\")",
1750
- "- Component table (e.g. weight allocation table, type mapping table)",
1751
- "- Rule description (e.g. \"Missing data dimensions use neutral value\")",
1752
- "```",
1753
- "",
1754
- "**Tightening rules**:",
1755
- "- The step-back question must be MORE ABSTRACT than the original problem, but cover all required information",
1756
- "- Abstract ≠ vague. Use precise formulas, principles, standards, definitions, or causal mechanisms",
1757
- "- Tighten to the MINIMUM generalisation layer that still covers the original problem",
1758
- "- Reasoning must RETURN to the original problem — do not stay at the principle level",
1759
- "",
1760
- "**Section 6 transition sentence (MANDATORY)**:",
1761
- "\"基于上述抽象框架,实现以下所有功能。\"",
1762
- "",
1763
- "### Generation Instructions",
1764
- "1. Extract 2-3 abstract principles/frameworks from the task domain",
1765
- "2. Build ASCII diagrams for each framework (formulas, tables, rules)",
1766
- "3. Apply the abstraction back to the concrete task",
1767
- "4. Verify: the generated prompt satisfies all structural rules in the skeleton above",
1768
- "5. Output only the generated prompt — no preamble, no meta-commentary",
1769
- ];
1770
- return lines.join("\n");
1771
- }
1772
- // ── Least-to-Most specialist ───────────────────────────────────────────────────
1773
- function compileLeastToMost(request, goalId, constraints, loopObjective, rollingText, analysis) {
1774
- const ctx = { request, goalId, constraints, loopObjective, rollingText };
1775
- const lines = [
1776
- ...buildHeader(ctx, "least-to-most"),
1777
- `**Rationale**: ${analysis.rationale}`,
1778
- "",
1779
- "Generate a complete prompt using the **Least-to-Most** technique. Follow the 8-section skeleton below.",
1780
- "Embed all structural constraints — do NOT reference an external file.",
1781
- "",
1782
- ...buildLoopObjectiveBlock(loopObjective),
1783
- ...buildConstraintsBlock(constraints),
1784
- ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []),
1785
- ...buildTaskBlock(ctx),
1786
- ...buildIdentityBlock(ctx),
1787
- "",
1788
- "### 8-Section Skeleton (REQUIRED)",
1789
- "",
1790
- "| # | Section | Required | Notes |",
1791
- "|---|---------|----------|-------|",
1792
- "| 1 | 角色 | ✓ | Domain expert role appropriate to the task |",
1793
- "| 2 | 任务 | ✓ | One-sentence task summary |",
1794
- "| 3 | 输入 | ✓ | Target module |",
1795
- "| 4 | 输出格式 | ✓ | Numbered list (e.g. \"1. DDL, 2. API, 3. Enum...\") |",
1796
- "| 5 | **Least-to-Most 逐步推理框架** | ✓ | See format rules below |",
1797
- "| 6 | 具体实现要求 | ✓ | Expand per output format list (NOT per sub-problem — sub-problems fully expanded in §5) |",
1798
- "| 7 | 硬约束 | ✓ | Numbered list |",
1799
- "| 8 | 生成要求 | ✓ | Acceptance criteria, must include: \"严格按照子问题顺序逐步实现\" |",
1800
- "",
1801
- "### Section 5 — Least-to-Most 逐步推理框架 (CRITICAL)",
1802
- "",
1803
- "Must contain **4-6 ordered sub-problems**. Each sub-problem format:",
1804
- "",
1805
- "```",
1806
- "### 子问题 N:[Sub-problem Name]",
1807
- "",
1808
- "**目标:** [What this sub-problem solves, which prior sub-problem(s) it depends on]",
1809
- "",
1810
- "**要求:**",
1811
- "- Specific requirement list",
1812
- "- [If sub-problem involves enums/mapping tables, list them]",
1813
- "",
1814
- "---",
1815
- "```",
1816
- "",
1817
- "**Critical rules**:",
1818
- "- Sub-problem 1 is the SIMPLEST (e.g. \"Define enums and base data structures\")",
1819
- "- Each sub-problem declares: \"基于子问题 N-1 的结论\"",
1820
- "- The LAST sub-problem MUST be: **\"综合实现完整模块\"** — list all components to integrate",
1821
- "- Sub-problems separated by `---`",
1822
- "- Order goes from LEAST complex → MOST complex, dependency chain must be explicit",
1823
- "- Each sub-problem must serve the original task — no unrelated steps",
1824
- "- The final sub-problem must be equivalent to or directly address the original task",
1825
- "",
1826
- "### Generation Instructions",
1827
- "1. Decompose the task into 4-6 ordered sub-problems with explicit dependencies",
1828
- "2. Sub-problem 1 starts with the simplest building block (data structures, enums, base config)",
1829
- "3. Each subsequent sub-problem builds on prior results",
1830
- "4. Final sub-problem integrates all components into the complete module",
1831
- "5. Verify: the generated prompt satisfies all structural rules in the skeleton above",
1832
- "6. Output only the generated prompt — no preamble, no meta-commentary",
1833
- ];
1834
- return lines.join("\n");
1835
- }
1836
- // ── Tree-of-Thought specialist ──────────────────────────────────────────────────
1837
- function compileToT(request, goalId, constraints, loopObjective, rollingText, analysis) {
1838
- const ctx = { request, goalId, constraints, loopObjective, rollingText };
1839
- const lines = [
1840
- ...buildHeader(ctx, "tree-of-thought"),
1841
- `**Rationale**: ${analysis.rationale}`,
1842
- "",
1843
- "Generate a complete prompt using the **Tree-of-Thought** technique. Follow the 8-section skeleton below.",
1844
- "Embed all structural constraints — do NOT reference an external file.",
1845
- "",
1846
- ...buildLoopObjectiveBlock(loopObjective),
1847
- ...buildConstraintsBlock(constraints),
1848
- ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []),
1849
- ...buildTaskBlock(ctx),
1850
- ...buildIdentityBlock(ctx),
1851
- "",
1852
- "### 8-Section Skeleton (REQUIRED)",
1853
- "",
1854
- "| # | Section | Required | Notes |",
1855
- "|---|---------|----------|-------|",
1856
- "| 1 | 角色 | ✓ | Multi-path problem-solving expert; may introduce 3 expert personas |",
1857
- "| 2 | 任务 | ✓ | One-sentence task summary |",
1858
- "| 3 | 输入 | ✓ | High-risk complex problem |",
1859
- "| 4 | 输出格式 | ✓ | \"先输出思维树过程,再输出最终答案/方案\" |",
1860
- "| 5 | **思维树探索框架** | ✓ | See 3 sub-blocks below (REQUIRED) |",
1861
- "| 6 | 具体实现要求 | ✓ | Expand per output format |",
1862
- "| 7 | 硬约束 | ✓ | Include branch_count, max_depth, pruning rules, safety/performance constraints |",
1863
- "| 8 | 生成要求 | ✓ | \"先探索多路径,再选择最优方案\" |",
1864
- "",
1865
- "### Section 5 — 思维树探索框架 (CRITICAL — 3 sub-blocks REQUIRED)",
1866
- "",
1867
- "**Sub-block A: Search Strategy Declaration**",
1868
- "```",
1869
- "搜索策略: [beam / dfs / expert-panel]",
1870
- "分支数(branch_count): 2-4",
1871
- "最大深度(max_depth): ≤3",
1872
- "每轮保留数(keep_count): 1-2",
1873
- "```",
1874
- "Strategy selection guide:",
1875
- "- **beam** (default): planning, creative, math search — generate multiple candidates per turn, keep top b",
1876
- "- **dfs**: puzzles, constraint satisfaction, debugging — go deep on highest-score branch, backtrack on failure",
1877
- "- **expert-panel**: 3+ experts each generate candidates, then cross-evaluate and revise",
1878
- "",
1879
- "**Sub-block B: Evaluation Criteria Table**",
1880
- "| 标准 | 权重 | 说明 |",
1881
- "|------|------|------|",
1882
- "| 正确性 | 最高 | Logic/math correctness |",
1883
- "| 可行性 | 高 | Can be actually implemented |",
1884
- "| 约束匹配 | 高 | Satisfies hard constraints |",
1885
- "| 性能 | 中 | Time/space efficiency |",
1886
- "| 安全性 | 最高 | No vulnerabilities/privilege escalation |",
1887
- "",
1888
- "**Sub-block C: Thought Tree State Table Format**",
1889
- "Require the model to output in this table format:",
1890
- "```",
1891
- "| 轮次 | 分支 | 候选方案 | 评估 | 决策 |",
1892
- "|------|------|---------|------|------|",
1893
- "| 1 | A | [方案描述] | 评分/判断 | 保留/剪枝 |",
1894
- "| 1 | B | [方案描述] | 评分/判断 | 保留/剪枝 |",
1895
- "| 1 | C | [方案描述] | 评分/判断 | 保留/剪枝 |",
1896
- "| 2 | A1 | [深入展开] | ... | ... |",
1897
- "| ... | ... | ... | ... | ... |",
1898
- "",
1899
- "最终选择: [最优方案 + 选择理由]",
1900
- "```",
1901
- "",
1902
- "### Critical Rules",
1903
- "- Hard constraints MUST be ranked FIRST in evaluation criteria",
1904
- "- Branch count: 2-4, depth: ≤3 (prevents token explosion)",
1905
- "- Every turn: generate multiple candidates → evaluate → keep/prune — never single-path",
1906
- "- If all branches are low quality: backtrack to previous turn and regenerate",
1907
- "- The core of ToT is NOT \"3 experts chatting\" — it's: candidate generation → state evaluation → search/prune → final selection",
1908
- "- thought is a PUBLIC intermediate semantic unit, not hidden chain-of-thought",
1909
- "- State table MUST have: round, branch ID, candidate description, evaluation score/judgment, keep/prune decision",
1910
- "",
1911
- "### Generation Instructions",
1912
- "1. Select the appropriate search strategy (beam/dfs/expert-panel) based on task type",
1913
- "2. Define 2-4 candidate approaches as initial branches",
1914
- "3. Build evaluation criteria table with hard constraints ranked first",
1915
- "4. Define the state table format for tracking exploration",
1916
- "5. Set branch count and max depth to prevent token explosion",
1917
- "6. Verify: the generated prompt satisfies all structural rules in the skeleton above",
1918
- "7. Output only the generated prompt — no preamble, no meta-commentary",
1919
- ];
1920
- return lines.join("\n");
1921
- }
1922
- // ═══════════════════════════════════════════════════════════════════════════
1923
- // Self-Evaluation block (v1.1 — autonomous loop feedback)
1924
- // ═══════════════════════════════════════════════════════════════════════════
1925
- /** Build the standardized self-evaluation block appended to every compiled prompt.
1926
- * The agent MUST output a JSON self-evaluation between the delimiters.
1927
- * 4 required fields + 3 optional evolution fields (P0–P2) —
1928
- * each consumed by at least one downstream function. */
1929
- export function buildSelfEvalBlock(round) {
1930
- return [
1931
- "",
1932
- "### LoopForge Self-Evaluation (REQUIRED)",
1933
- "",
1934
- `You are completing Round ${round}. After finishing the task above, ` +
1935
- "you MUST output a self-evaluation in this exact format:",
1936
- "",
1937
- "```",
1938
- "---loopforge-eval",
1939
- "{",
1940
- ' "success": true,',
1941
- ` "output_summary": "<one paragraph — what was DONE in round ${round}, be specific>",`,
1942
- ' "constraint_violations": [],',
1943
- ' "should_continue": true,',
1944
- ' "discovered_constraints": [],',
1945
- ' "objective_refinement": "",',
1946
- ' "emerged_subtasks": [],',
1947
- ' "execution_evidence": {',
1948
- ' "files_changed": [],',
1949
- ' "test_results": {"passed": 0, "failed": 0, "skipped": 0},',
1950
- ' "success_criteria_met": [],',
1951
- ' "success_criteria_remaining": [],',
1952
- ' "progress_estimate": 0.0',
1953
- ' },',
1954
- ' "retracted_constraints": [],',
1955
- ' "revised_success_criteria": [],',
1956
- ' "wrong_assumptions": [],',
1957
- ' "compression_checkpoint": false,',
1958
- ' "checkpoint_label": ""',
1959
- "}",
1960
- "---end-loopforge-eval",
1961
- "```",
1962
- "",
1963
- "Field rules:",
1964
- `- success: true ONLY if all hard constraints were met and the task goal was achieved`,
1965
- `- output_summary: Be specific about what was PRODUCED, not what you "tried". ` +
1966
- `Bad: "worked on audit". Good: "Found 3 vulns: reentrancy in withdraw(), ` +
1967
- `integer overflow in transfer(), missing access control in mint()". ` +
1968
- `This feeds cross-round knowledge distillation.`,
1969
- `- constraint_violations: List ONLY constraints you actually broke. Empty array [] if none. ` +
1970
- `This directly affects constraint-integrity scoring and retirement decisions.`,
1971
- `- should_continue: false ONLY when the ENTIRE task is complete. ` +
1972
- `If there is more to audit/implement/test, say true. ` +
1973
- `This tells the autonomous runner when to stop.`,
1974
- `- discovered_constraints (P0 — optional): New constraints you discovered this round. ` +
1975
- `Omit or [] if none.`,
1976
- `- objective_refinement (P1 — optional): Deepened understanding of the task goal. ` +
1977
- `APPENDED to the original objective. Omit or "" if unchanged.`,
1978
- `- emerged_subtasks (P2 — optional): Sub-problems that surfaced during execution. ` +
1979
- `Omit or [] if none.`,
1980
- `- execution_evidence (P4 — recommended): Structured record of what you actually did. ` +
1981
- `files_changed: paths relative to project root. test_results: null if no tests run. ` +
1982
- `success_criteria_met/remaining: track against the Loop Objective. ` +
1983
- `progress_estimate: your honest estimate of overall completion (0.0 to 1.0). ` +
1984
- `This enables real progress tracking and early stall detection.`,
1985
- `- retracted_constraints (P5 — optional): Constraints you now believe are WRONG. ` +
1986
- `Removed from active guardrails. Only retract if you have evidence. Omit or [] if none.`,
1987
- `- revised_success_criteria (P5 — optional): Success criteria that need reformulation. ` +
1988
- `Array of {old, new} objects. Applied to the Loop Objective. Omit or [] if none.`,
1989
- `- wrong_assumptions (P5 — optional): Assumptions from earlier rounds that turned ` +
1990
- `out to be incorrect. Recorded as key lessons. Omit or [] if none.`,
1991
- `- compression_checkpoint (v1.10 — optional): Set to true when you have COMPLETED a ` +
1992
- `major subtask. The compiler will snapshot the current state (constraints, outcome) ` +
1993
- `into a persistent checkpoint that survives context-window eviction. ` +
1994
- `Use sparingly — only at genuine subtask boundaries, not every round. ` +
1995
- `Omit or false otherwise.`,
1996
- `- checkpoint_label (v1.10 — optional): Human-readable label for this checkpoint. ` +
1997
- `e.g. "数据模型层完成", "API layer complete", "Auth module done". ` +
1998
- `Used as the checkpoint heading in subsequent prompts. Only meaningful when ` +
1999
- `compression_checkpoint is true. Omit or "" otherwise.`,
2000
- `- The JSON MUST appear between the ---loopforge-eval and ---end-loopforge-eval markers`,
2001
- `- Do NOT wrap the markers in code fences — output them as raw text`,
2002
- "",
2003
- ].join("\n");
2004
- }
2005
- // ═══════════════════════════════════════════════════════════════════════════
2006
- // Top-level compile — ties layers together
2007
- // ═══════════════════════════════════════════════════════════════════════════
2008
- export function compileLoop(request, vaultContext = null) {
2009
- // Layer 1: Decide compile level
2010
- const level = decideLevel(request, vaultContext);
2011
- // Compile at the decided level
2012
- let response;
2013
- if (level === "l0") {
2014
- response = compileL0(request, vaultContext);
2015
- }
2016
- else if (level === "l1") {
2017
- response = compileL1(request, vaultContext);
2018
- }
2019
- else {
2020
- response = compileL2(request, vaultContext);
2021
- }
2022
- // Layer 2: Compute advisories
2023
- const { warnings, suggestedNextTask, alignment, health } = computeAdvisories(request, vaultContext);
2024
- // Merge advisories into response (preserve P4 consistency warnings from L2)
2025
- response.warnings = [...(response.warnings || []), ...warnings];
2026
- // Only overwrite suggested_next_task from advisories if the compile function
2027
- // didn't already set one (e.g. L2 sets it from emerged_subtasks)
2028
- if (!response.suggested_next_task) {
2029
- response.suggested_next_task = suggestedNextTask;
2030
- }
2031
- response.task_alignment = alignment;
2032
- response.loop_health = health;
2033
- response.recompile_level = level;
2034
- // v1.1: Append self-evaluation block for autonomous loop feedback
2035
- response.prompt += buildSelfEvalBlock(request.round);
2036
- // Multi-agent: Inject delegation history if available
2037
- if (vaultContext) {
2038
- const delegationText = buildDelegationSummary(vaultContext);
2039
- if (delegationText) {
2040
- response.prompt += delegationText;
2041
- }
2042
- }
328
+ const policy = getPolicy();
329
+ const statePath = `${policy.state_file.directory}/${request.loop_id}-state.md`;
330
+ const state = createCanonicalLoopState(request, response, statePath);
331
+ const markdown = renderCanonicalStateMarkdown(state);
332
+ const artifact = assemblePromptArtifact({
333
+ state,
334
+ level: decision.level,
335
+ reasons: decision.reasons,
336
+ mode: policy.prompt.injection_mode,
337
+ budgets: {
338
+ l0: policy.prompt.l0_max_chars,
339
+ l1: policy.prompt.l1_max_chars,
340
+ l2: policy.prompt.l2_max_chars,
341
+ },
342
+ attempt: request.attempt,
343
+ selfEvaluationBlock: buildSelfEvalBlock(request.round),
344
+ fullStateMarkdown: markdown,
345
+ });
346
+ response.prompt = artifact.renderedPrompt;
347
+ response.prompt_artifact = artifact;
348
+ response.state_file_content = policy.state_file.enabled ? markdown : undefined;
2043
349
  return response;
2044
350
  }
2045
351
  //# sourceMappingURL=loop-compiler.js.map