loopforge 1.7.0 → 1.12.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 (88) hide show
  1. package/README.md +64 -3
  2. package/dist/backends/fs.d.ts.map +1 -1
  3. package/dist/backends/fs.js +1 -2
  4. package/dist/backends/fs.js.map +1 -1
  5. package/dist/backends/interface.d.ts +2 -0
  6. package/dist/backends/interface.d.ts.map +1 -1
  7. package/dist/builder.d.ts +6 -10
  8. package/dist/builder.d.ts.map +1 -1
  9. package/dist/builder.js +128 -63
  10. package/dist/builder.js.map +1 -1
  11. package/dist/engine.d.ts +16 -2
  12. package/dist/engine.d.ts.map +1 -1
  13. package/dist/engine.js +142 -54
  14. package/dist/engine.js.map +1 -1
  15. package/dist/index.d.ts +7 -6
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +5 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/loop-compiler.d.ts +33 -2
  20. package/dist/loop-compiler.d.ts.map +1 -1
  21. package/dist/loop-compiler.js +391 -135
  22. package/dist/loop-compiler.js.map +1 -1
  23. package/dist/mcp/server.d.ts.map +1 -1
  24. package/dist/mcp/server.js +6 -4
  25. package/dist/mcp/server.js.map +1 -1
  26. package/dist/mcp/session.d.ts +18 -4
  27. package/dist/mcp/session.d.ts.map +1 -1
  28. package/dist/mcp/session.js +225 -24
  29. package/dist/mcp/session.js.map +1 -1
  30. package/dist/mcp/tools.d.ts +32 -1
  31. package/dist/mcp/tools.d.ts.map +1 -1
  32. package/dist/mcp/tools.js +33 -14
  33. package/dist/mcp/tools.js.map +1 -1
  34. package/dist/memory-bridge.d.ts +47 -0
  35. package/dist/memory-bridge.d.ts.map +1 -0
  36. package/dist/memory-bridge.js +290 -0
  37. package/dist/memory-bridge.js.map +1 -0
  38. package/dist/observability.d.ts +1 -1
  39. package/dist/observability.js +1 -1
  40. package/dist/policy.d.ts +66 -9
  41. package/dist/policy.d.ts.map +1 -1
  42. package/dist/policy.js +41 -28
  43. package/dist/policy.js.map +1 -1
  44. package/dist/protocol.d.ts +120 -16
  45. package/dist/protocol.d.ts.map +1 -1
  46. package/dist/protocol.js +23 -9
  47. package/dist/protocol.js.map +1 -1
  48. package/dist/replay.js +7 -7
  49. package/dist/replay.js.map +1 -1
  50. package/dist/runtime.d.ts +20 -3
  51. package/dist/runtime.d.ts.map +1 -1
  52. package/dist/runtime.js +420 -177
  53. package/dist/runtime.js.map +1 -1
  54. package/dist/tests/builder.test.js +40 -28
  55. package/dist/tests/builder.test.js.map +1 -1
  56. package/dist/tests/engine.test.js +13 -17
  57. package/dist/tests/engine.test.js.map +1 -1
  58. package/dist/tests/generate-schema.test.js +10 -7
  59. package/dist/tests/generate-schema.test.js.map +1 -1
  60. package/dist/tests/loop-compiler.test.js +481 -27
  61. package/dist/tests/loop-compiler.test.js.map +1 -1
  62. package/dist/tests/mcp.test.js +103 -103
  63. package/dist/tests/mcp.test.js.map +1 -1
  64. package/dist/tests/memory-bridge.test.d.ts +8 -0
  65. package/dist/tests/memory-bridge.test.d.ts.map +1 -0
  66. package/dist/tests/memory-bridge.test.js +383 -0
  67. package/dist/tests/memory-bridge.test.js.map +1 -0
  68. package/dist/tests/policy.test.js +5 -15
  69. package/dist/tests/policy.test.js.map +1 -1
  70. package/dist/tests/protocol.test.js +15 -3
  71. package/dist/tests/protocol.test.js.map +1 -1
  72. package/dist/tests/replay.test.js +9 -9
  73. package/dist/tests/replay.test.js.map +1 -1
  74. package/dist/tests/runtime.test.js +8 -8
  75. package/dist/tests/runtime.test.js.map +1 -1
  76. package/dist/tests/verification-gate.test.js +1 -1
  77. package/dist/tests/verification-gate.test.js.map +1 -1
  78. package/dist/verification-gate.d.ts +2 -2
  79. package/dist/verification-gate.js +2 -2
  80. package/loop_policy.json +28 -23
  81. package/package.json +1 -1
  82. package/skills/perception/SKILL.md +64 -12
  83. package/dist/adapter.d.ts +0 -17
  84. package/dist/adapter.d.ts.map +0 -1
  85. package/dist/adapter.js +0 -228
  86. package/dist/adapter.js.map +0 -1
  87. package/dist/tests/adapter.test.d.ts +0 -2
  88. package/dist/tests/adapter.test.d.ts.map +0 -1
@@ -12,7 +12,7 @@
12
12
  import { createHash } from "node:crypto";
13
13
  import { readFileSync } from "node:fs";
14
14
  import { getPolicy } from "./policy.js";
15
- import { makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeRollingSummary, makeTaskAlignment, } from "./protocol.js";
15
+ import { AgentStatus, makeCheckpointSummary, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeRollingSummary, makeTaskAlignment, } from "./protocol.js";
16
16
  import { routeTechniqueAdaptive } from "./builder.js";
17
17
  // ═══════════════════════════════════════════════════════════════════════════
18
18
  // Repair cue detection
@@ -34,7 +34,7 @@ function detectsRepairSignal(request) {
34
34
  // ═══════════════════════════════════════════════════════════════════════════
35
35
  // Tokenization helpers
36
36
  // ═══════════════════════════════════════════════════════════════════════════
37
- function tokenize(text) {
37
+ export function tokenize(text) {
38
38
  const tokens = text.split(/\s+/);
39
39
  const result = new Set();
40
40
  for (let token of tokens) {
@@ -52,7 +52,7 @@ function tokenize(text) {
52
52
  }
53
53
  return result;
54
54
  }
55
- function jaccard(a, b) {
55
+ export function jaccard(a, b) {
56
56
  if (a.size === 0 || b.size === 0)
57
57
  return 0.0;
58
58
  let intersection = 0;
@@ -64,6 +64,141 @@ function jaccard(a, b) {
64
64
  return union > 0 ? intersection / union : 0.0;
65
65
  }
66
66
  // ═══════════════════════════════════════════════════════════════════════════
67
+ // Delegation helpers (v1.9 — AgentTool mode lightweight utilities)
68
+ // ═══════════════════════════════════════════════════════════════════════════
69
+ /** Filter relevant constraints for a sub-agent task using Jaccard token similarity.
70
+ * Returns constraints whose token overlap with the subTask exceeds the threshold.
71
+ * Default threshold 0.15 is intentionally lower than the 0.3/0.5 alignment thresholds
72
+ * — constraint filtering should err on the side of inclusion. */
73
+ export function filterConstraintsForSubTask(allConstraints, subTask, threshold = 0.15) {
74
+ if (!allConstraints.length || !subTask.trim())
75
+ return [];
76
+ const taskTokens = tokenize(subTask);
77
+ if (taskTokens.size === 0)
78
+ return [];
79
+ return allConstraints.filter((c) => {
80
+ const cTokens = tokenize(c);
81
+ return jaccard(taskTokens, cTokens) >= threshold;
82
+ });
83
+ }
84
+ /** Format a self-contained delegation prompt for a sub-agent.
85
+ * Produces a prompt that stands alone — no references to parent conversation,
86
+ * no "based on above", no "continue from previous". This matches the AgentTool
87
+ * contract: "Workers can't see your conversation." */
88
+ export function formatDelegationPrompt(subTask, subAgentType, relevantConstraints, options) {
89
+ const lines = [];
90
+ // Header
91
+ lines.push("### Delegated Task");
92
+ lines.push("");
93
+ lines.push(subTask);
94
+ lines.push("");
95
+ // Context (optional — e.g. file paths, module names)
96
+ if (options?.context) {
97
+ lines.push("### Context");
98
+ lines.push("");
99
+ lines.push(options.context);
100
+ lines.push("");
101
+ }
102
+ // Constraints (only for types that need them — Explore is read-only)
103
+ if (relevantConstraints.length && subAgentType !== "explore") {
104
+ lines.push("### Relevant Constraints");
105
+ for (const c of relevantConstraints)
106
+ lines.push(`- ${c}`);
107
+ lines.push("");
108
+ }
109
+ // Output format (for Explore and Plan agents)
110
+ if (subAgentType === "explore") {
111
+ if (options?.outputFormat) {
112
+ lines.push("### Output Format");
113
+ lines.push("");
114
+ lines.push(options.outputFormat);
115
+ lines.push("");
116
+ }
117
+ else {
118
+ lines.push("### Output Format");
119
+ lines.push("");
120
+ lines.push("Report findings as a structured list with file paths and line numbers.");
121
+ lines.push("");
122
+ }
123
+ }
124
+ if (subAgentType === "plan") {
125
+ lines.push("### Instructions");
126
+ lines.push("");
127
+ lines.push("Design a concrete implementation plan. Output must include:");
128
+ lines.push("- Step-by-step approach");
129
+ lines.push("- Critical files for implementation (3-5 files with paths)");
130
+ lines.push("- Key design decisions and tradeoffs");
131
+ lines.push("");
132
+ }
133
+ if (subAgentType === "general-purpose") {
134
+ lines.push("### Instructions");
135
+ lines.push("");
136
+ lines.push("Complete the task fully — don't gold-plate, but don't leave it half-done.");
137
+ lines.push("Report what was done, what files were changed, and any issues encountered.");
138
+ lines.push("");
139
+ }
140
+ if (!["explore", "general-purpose", "plan"].includes(subAgentType)) {
141
+ // Custom sub-agent type — generic template
142
+ lines.push("### Instructions");
143
+ lines.push("");
144
+ lines.push("Complete the task above and report results concisely.");
145
+ lines.push("");
146
+ }
147
+ return lines.join("\n");
148
+ }
149
+ /** Build a delegation history summary from vault context (v1.9 — multi-agent).
150
+ * Scans vault for delegation_journal entries and formats them as a table.
151
+ * Returns empty string if no delegation history exists. */
152
+ export function buildDelegationSummary(vaultContext) {
153
+ if (!vaultContext)
154
+ return "";
155
+ const results = vaultContext.results;
156
+ if (!results?.length)
157
+ return "";
158
+ const delegations = results.filter((r) => r?.task_type === "delegation_journal");
159
+ if (!delegations.length)
160
+ return "";
161
+ // Helper: escape pipe characters so they don't break the markdown table
162
+ const esc = (s) => s.replace(/\|/g, "\\|");
163
+ // Collect all delegation entries across rounds
164
+ const entries = [];
165
+ for (const d of delegations) {
166
+ const lineage = d.loop_lineage;
167
+ const round = typeof lineage?.round === "number" ? lineage.round : 0;
168
+ const dels = lineage?.delegations;
169
+ if (!dels?.length)
170
+ continue;
171
+ for (const e of dels) {
172
+ entries.push({
173
+ round,
174
+ agentId: typeof e.agentId === "string" ? e.agentId : (typeof e.index === "number" ? `worker-${e.index}` : "?"),
175
+ type: typeof e.subAgentType === "string" ? e.subAgentType : "?",
176
+ task: typeof e.subTask === "string" ? e.subTask : "?",
177
+ result: typeof e.resultSummary === "string" ? e.resultSummary : "?",
178
+ success: typeof e.success === "boolean" ? e.success : true,
179
+ });
180
+ }
181
+ }
182
+ if (!entries.length)
183
+ return "";
184
+ const lines = [
185
+ "",
186
+ "### Delegation History (Multi-Agent)",
187
+ "",
188
+ "| Round | Agent | Type | Task | Result | ✓/✗ |",
189
+ "|-------|-------|------|------|--------|-----|",
190
+ ];
191
+ for (const e of entries) {
192
+ const status = e.success ? "✓" : "✗";
193
+ const shortAgent = e.agentId.length > 24 ? e.agentId.slice(0, 21) + "..." : e.agentId;
194
+ const shortTask = e.task.length > 50 ? e.task.slice(0, 47) + "..." : e.task;
195
+ const shortResult = e.result.length > 40 ? e.result.slice(0, 37) + "..." : e.result;
196
+ lines.push(`| R${e.round} | ${esc(shortAgent)} | ${e.type} | ${esc(shortTask)} | ${esc(shortResult)} | ${status} |`);
197
+ }
198
+ lines.push("");
199
+ return lines.join("\n");
200
+ }
201
+ // ═══════════════════════════════════════════════════════════════════════════
67
202
  // Goal identity
68
203
  // ═══════════════════════════════════════════════════════════════════════════
69
204
  export function computeGoalTextHash(task) {
@@ -77,17 +212,6 @@ export function deriveGoalId(loopId, task, explicitGoalId = "") {
77
212
  taskPrefix = taskPrefix.replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
78
213
  return loopId ? `${loopId}:${taskPrefix}` : taskPrefix;
79
214
  }
80
- function makePreviousRound() {
81
- return {
82
- goal_id: "",
83
- goal_text_hash: "",
84
- quality_score: 0,
85
- success: true,
86
- task: "",
87
- constraints_active: [],
88
- prompt_text: "",
89
- };
90
- }
91
215
  export function getPreviousRound(loopId, roundNum, vaultContext) {
92
216
  if (vaultContext === null)
93
217
  return null;
@@ -100,8 +224,7 @@ export function getPreviousRound(loopId, roundNum, vaultContext) {
100
224
  return {
101
225
  goal_id: lineage.goal_id ?? "",
102
226
  goal_text_hash: lineage.goal_text_hash ?? "",
103
- quality_score: lineage.quality_score ?? 0,
104
- success: r.success ?? true,
227
+ success: r.success ?? lineage.success ?? true,
105
228
  task: r.task ?? r.user_intent ?? "",
106
229
  constraints_active: lineage.constraints_active ?? [],
107
230
  prompt_text: r.full_prompt ?? "",
@@ -119,7 +242,7 @@ function getRecentRounds(loopId, n, vaultContext) {
119
242
  const lineage = (r.loop_lineage || r.lineage || {});
120
243
  if (lineage.loop_id === loopId) {
121
244
  rounds.push({
122
- quality_score: lineage.quality_score ?? 0,
245
+ success: r.success ?? lineage.success ?? false,
123
246
  round: lineage.round ?? 0,
124
247
  goal_text_hash: lineage.goal_text_hash ?? "",
125
248
  });
@@ -179,7 +302,7 @@ function strategyCollapse(loopId, vaultContext) {
179
302
  const recent = getRecentRounds(loopId, 3, vaultContext);
180
303
  if (recent.length < 3)
181
304
  return false;
182
- return recent.every((r) => r.quality_score < 3);
305
+ return recent.every((r) => r.success === false);
183
306
  }
184
307
  // ═══════════════════════════════════════════════════════════════════════════
185
308
  // Constraint Retirement (v3.5)
@@ -288,7 +411,7 @@ function countProgressStallRounds(loopId, currentRound, vaultContext, threshold)
288
411
  }
289
412
  /** Detect repeated failure patterns in sampled rounds.
290
413
  * A failure pattern = 2+ consecutive rounds where:
291
- * 1. quality_score < 3 (failed or weak)
414
+ * 1. success === false (failed)
292
415
  * 2. same technique was used
293
416
  * 3. task text is similar (Jaccard > 0.4)
294
417
  *
@@ -300,7 +423,7 @@ function detectFailurePatterns(sampled) {
300
423
  let runStart = -1;
301
424
  for (let i = 0; i < sampled.length; i++) {
302
425
  const r = sampled[i];
303
- const isFailed = r.quality_score < 3;
426
+ const isFailed = r.success === false;
304
427
  if (isFailed && runStart === -1) {
305
428
  runStart = i;
306
429
  }
@@ -321,7 +444,7 @@ function detectFailurePatterns(sampled) {
321
444
  }
322
445
  return patterns;
323
446
  }
324
- /** Classify a run of consecutive low-quality rounds as a failure pattern.
447
+ /** Classify a run of consecutive failed rounds as a failure pattern.
325
448
  * Returns null if the rounds are too dissimilar to be the same pattern. */
326
449
  function classifyRun(run) {
327
450
  if (run.length < 2)
@@ -341,9 +464,9 @@ function classifyRun(run) {
341
464
  }
342
465
  const rounds = `R${run[0].round}-R${run[run.length - 1].round}`;
343
466
  const taskPreview = run[0].task.slice(0, 80);
344
- const avgScore = (run.reduce((s, r) => s + r.quality_score, 0) / run.length).toFixed(1);
467
+ const failureRate = `${run.length}/${run.length}`;
345
468
  return (`technique '${technique}' on task '${taskPreview}' ` +
346
- `failed ${run.length} consecutive rounds (${rounds}, avg score ${avgScore}) — ` +
469
+ `failed ${run.length} consecutive rounds (${rounds}) — ` +
347
470
  `consider strategy change`);
348
471
  }
349
472
  /** Extract round numbers referenced in failure pattern descriptions.
@@ -370,7 +493,7 @@ function extractFailureRoundNums(patterns) {
370
493
  // ═══════════════════════════════════════════════════════════════════════════
371
494
  // Rolling Summary (v3.5)
372
495
  // ═══════════════════════════════════════════════════════════════════════════
373
- export function buildRollingSummary(loopId, currentRound, vaultContext) {
496
+ export function buildRollingSummary(loopId, currentRound, vaultContext, sinceRound) {
374
497
  if (vaultContext === null)
375
498
  return null;
376
499
  const results = vaultContext.results || [];
@@ -378,6 +501,7 @@ export function buildRollingSummary(loopId, currentRound, vaultContext) {
378
501
  return null;
379
502
  const policy = getPolicy();
380
503
  const window = policy.summary.window;
504
+ const minRound = sinceRound ?? 0;
381
505
  // Collect rounds matching this loop_id, excluding current
382
506
  const rounds = [];
383
507
  for (const r of results) {
@@ -387,9 +511,11 @@ export function buildRollingSummary(loopId, currentRound, vaultContext) {
387
511
  const rnd = lineage.round;
388
512
  if (rnd >= currentRound)
389
513
  continue;
514
+ if (rnd < minRound)
515
+ continue;
390
516
  rounds.push({
391
517
  round: rnd,
392
- quality_score: lineage.quality_score ?? 0,
518
+ success: r.success ?? lineage.success ?? false,
393
519
  task: r.task ?? r.user_intent ?? "",
394
520
  output_summary: r.output_summary ?? "",
395
521
  constraint_violations: r.constraint_violations || [],
@@ -401,32 +527,13 @@ export function buildRollingSummary(loopId, currentRound, vaultContext) {
401
527
  // Sort by round descending, take last N
402
528
  rounds.sort((a, b) => b.round - a.round);
403
529
  const sampled = rounds.slice(0, window).reverse(); // chronological order
404
- // Quality trajectory
405
- const trajectory = sampled.map((r) => r.quality_score);
406
- // Trajectory direction
407
- let direction = "stable";
408
- if (trajectory.length >= 2) {
409
- const diffs = trajectory.slice(1).map((v, i) => v - trajectory[i]);
410
- if (diffs.every((d) => d >= 0) && diffs.some((d) => d > 0)) {
411
- direction = "improving";
412
- }
413
- else if (diffs.every((d) => d <= 0) && diffs.some((d) => d < 0)) {
414
- direction = "declining";
415
- }
416
- else if (diffs.every((d) => d === 0)) {
417
- direction = "stable";
418
- }
419
- else {
420
- direction = "volatile";
421
- }
422
- }
423
- // What worked
424
- const whatWorked = [];
530
+ // v1.12: Key outcomes — all rounds with output_summary
531
+ const keyOutcomes = [];
425
532
  for (const r of sampled) {
426
- if (r.quality_score >= 4 &&
427
- r.output_summary) {
428
- whatWorked.push(`R${r.round} (score=${r.quality_score}, ` +
429
- `${r.technique_used || "n/a"}): ${String(r.output_summary).slice(0, 150)}`);
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)}`);
430
537
  }
431
538
  }
432
539
  // Recurring issues
@@ -445,17 +552,10 @@ export function buildRollingSummary(loopId, currentRound, vaultContext) {
445
552
  .filter(([, count]) => count >= 2)
446
553
  .sort((a, b) => b[1] - a[1])
447
554
  .map(([v, count]) => `${v} (appeared in ${count} rounds)`);
448
- // Key lessons
449
- const keyLessons = [];
450
- for (const r of sampled) {
451
- if (r.quality_score >= 4 && r.output_summary) {
452
- keyLessons.push(`[R${r.round}] ${String(r.output_summary).slice(0, 200)}`);
453
- }
454
- }
455
555
  // v1.7: Failure lineage weighting — detect and demote failure patterns
456
556
  const sampledRounds = sampled.map((r) => ({
457
557
  round: r.round,
458
- quality_score: r.quality_score,
558
+ success: r.success,
459
559
  task: r.task,
460
560
  output_summary: r.output_summary,
461
561
  constraint_violations: r.constraint_violations,
@@ -463,21 +563,21 @@ export function buildRollingSummary(loopId, currentRound, vaultContext) {
463
563
  }));
464
564
  const failurePatterns = detectFailurePatterns(sampledRounds);
465
565
  const failedRoundNums = extractFailureRoundNums(failurePatterns);
466
- // Re-weight key_lessons: lessons from failure-pattern rounds go to end
467
- if (failedRoundNums.size > 0 && keyLessons.length > 0) {
566
+ // Re-weight key_outcomes: outcomes from failure-pattern rounds go to end
567
+ if (failedRoundNums.size > 0 && keyOutcomes.length > 0) {
468
568
  const promoted = [];
469
569
  const demoted = [];
470
- for (const kl of keyLessons) {
471
- const rndMatch = kl.match(/^\[R(\d+)\]/);
570
+ for (const ko of keyOutcomes) {
571
+ const rndMatch = ko.match(/^\[R(\d+)\]/);
472
572
  if (rndMatch && failedRoundNums.has(parseInt(rndMatch[1], 10))) {
473
- demoted.push(kl.replace(/^\[R\d+\]/, "$& [Consider alternatives]"));
573
+ demoted.push(ko.replace(/^\[R\d+\]/, "$& [Consider alternatives]"));
474
574
  }
475
575
  else {
476
- promoted.push(kl);
576
+ promoted.push(ko);
477
577
  }
478
578
  }
479
- keyLessons.length = 0;
480
- keyLessons.push(...promoted, ...demoted);
579
+ keyOutcomes.length = 0;
580
+ keyOutcomes.push(...promoted, ...demoted);
481
581
  }
482
582
  // Re-weight recurring_issues: mark issues that ONLY appear in failure-pattern rounds
483
583
  if (failedRoundNums.size > 0 && recurringIssues.length > 0) {
@@ -504,11 +604,8 @@ export function buildRollingSummary(loopId, currentRound, vaultContext) {
504
604
  }));
505
605
  }
506
606
  return makeRollingSummary({
507
- quality_trajectory: trajectory,
508
- trajectory_direction: direction,
509
- what_worked: whatWorked,
607
+ key_outcomes: keyOutcomes,
510
608
  recurring_issues: recurringIssues,
511
- key_lessons: keyLessons,
512
609
  rounds_sampled: sampled.length,
513
610
  generated_at_round: currentRound,
514
611
  failed_patterns: failurePatterns.length ? failurePatterns : [],
@@ -520,14 +617,13 @@ export function formatRollingSummaryForPrompt(rs) {
520
617
  const lines = [
521
618
  "### Cross-Round Summary (Accumulated)",
522
619
  "",
523
- `**Sampled**: ${rs.rounds_sampled} prior rounds | **Direction**: ${rs.trajectory_direction}`,
524
- `**Quality Trajectory**: [${rs.quality_trajectory.join(", ")}]`,
620
+ `**Sampled**: ${rs.rounds_sampled} prior rounds`,
525
621
  "",
526
622
  ];
527
- if (rs.what_worked.length) {
528
- lines.push("**What Worked (score >= 4)**:");
529
- for (const w of rs.what_worked)
530
- lines.push(`- ${w}`);
623
+ if (rs.key_outcomes.length) {
624
+ lines.push("**Key Outcomes**:");
625
+ for (const ko of rs.key_outcomes)
626
+ lines.push(`- ${ko}`);
531
627
  lines.push("");
532
628
  }
533
629
  if (rs.recurring_issues.length) {
@@ -541,21 +637,137 @@ export function formatRollingSummaryForPrompt(rs) {
541
637
  lines.push("### ⚠️ Failure Patterns — Consider Strategy Change");
542
638
  lines.push("");
543
639
  lines.push("The following approaches failed repeatedly. The compiler has demoted " +
544
- "their influence on key lessons. Consider a different technique or " +
640
+ "their influence on key outcomes. Consider a different technique or " +
545
641
  "narrower scope.");
546
642
  for (const fp of rs.failed_patterns) {
547
643
  lines.push(`- 🚫 ${fp}`);
548
644
  }
549
645
  lines.push("");
550
646
  }
551
- if (rs.key_lessons.length) {
552
- lines.push("**Key Lessons From High-Score Rounds**:");
553
- for (const kl of rs.key_lessons)
554
- lines.push(`- ${kl}`);
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
+ }
555
705
  lines.push("");
556
706
  }
557
707
  return lines.join("\n");
558
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)
713
+ 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
+ }
730
+ }
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 ?? [],
739
+ });
740
+ }
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");
770
+ }
559
771
  // ═══════════════════════════════════════════════════════════════════════════
560
772
  // LAYER 1 — Hard Gates (can change compile level)
561
773
  // ═══════════════════════════════════════════════════════════════════════════
@@ -699,7 +911,7 @@ export function checkLoopHealth(loopId, request, vaultContext) {
699
911
  // 4. strategy_stability
700
912
  const recent = getRecentRounds(loopId, 3, vaultContext);
701
913
  const strategyStability = recent.length > 0
702
- ? recent.every((r) => r.quality_score >= 4)
914
+ ? recent.every((r) => r.success === true)
703
915
  : true;
704
916
  // 5. task_continuity
705
917
  let taskContinuity = 1.0;
@@ -736,15 +948,22 @@ function computeSuggestedNextTask(loopId, vaultContext) {
736
948
  if (vaultContext === null)
737
949
  return "";
738
950
  const results = vaultContext.results || [];
951
+ let bestTask = "";
952
+ let bestRound = -1;
739
953
  for (const r of results) {
740
954
  const lineage = (r.loop_lineage || r.lineage || {});
741
- if (lineage.loop_id === loopId) {
955
+ if (lineage.loop_id !== loopId)
956
+ continue;
957
+ const rnd = lineage.round ?? 0;
958
+ if (rnd > bestRound) {
742
959
  const task = r.task ?? r.user_intent ?? "";
743
- if (task)
744
- return `Previous round focused on: ${task.slice(0, 120)}`;
960
+ if (task) {
961
+ bestTask = task;
962
+ bestRound = rnd;
963
+ }
745
964
  }
746
965
  }
747
- return "";
966
+ return bestTask ? `[R${bestRound}] Previous task: ${bestTask.slice(0, 120)}` : "";
748
967
  }
749
968
  export function computeAdvisories(request, vaultContext) {
750
969
  const warnings = [];
@@ -760,7 +979,7 @@ export function computeAdvisories(request, vaultContext) {
760
979
  }
761
980
  // Strategy collapse check
762
981
  if (strategyCollapse(request.loop_id, vaultContext)) {
763
- warnings.push("strategy_collapse: 3 consecutive low-quality rounds — " +
982
+ warnings.push("strategy_collapse: 3 consecutive failed rounds — " +
764
983
  "consider force_level=L2 rebuild");
765
984
  }
766
985
  // Repair cue detection
@@ -959,7 +1178,7 @@ function compileL0(request, vaultContext, prevRound) {
959
1178
  const cachedPrompt = prev?.prompt_text ?? "";
960
1179
  if (cachedPrompt) {
961
1180
  return makeLoopCompileResponse({
962
- status: "ok",
1181
+ status: AgentStatus.OK,
963
1182
  prompt: cachedPrompt,
964
1183
  recompile_level: "l0",
965
1184
  diff_from_previous: `L0 cache hit — reusing prompt from round ${request.round - 1}`,
@@ -1053,7 +1272,7 @@ function compileL1(request, vaultContext, prevRound) {
1053
1272
  lines.push("### Task");
1054
1273
  lines.push(request.task);
1055
1274
  return makeLoopCompileResponse({
1056
- status: "ok",
1275
+ status: AgentStatus.OK,
1057
1276
  prompt: lines.join("\n"),
1058
1277
  recompile_level: "l1",
1059
1278
  diff_from_previous: diffParts.length ? diffParts.join("; ") : "Patch applied.",
@@ -1072,19 +1291,38 @@ function compileL1(request, vaultContext, prevRound) {
1072
1291
  }
1073
1292
  export function compileL2(request, vaultContext) {
1074
1293
  const goalId = deriveGoalId(request.loop_id, request.task, request.goal_id);
1075
- // v3.5: Adaptive technique routing
1076
- const analysis = routeTechniqueAdaptive(request.task, vaultContext, request.loop_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);
1077
1297
  const technique = analysis.technique;
1078
1298
  const referenceFile = analysis.reference_file;
1079
- // v3.5: Rolling summary
1080
- const rollingSummary = buildRollingSummary(request.loop_id, request.round, vaultContext);
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);
1081
1320
  // P5c: Wrong assumptions — must be applied BEFORE formatting so they appear
1082
1321
  // in the rendered cross-round summary text injected into the prompt.
1083
- const lr = request.last_round_result;
1084
1322
  if (lr?.wrong_assumptions?.length) {
1085
1323
  if (rollingSummary) {
1086
- rollingSummary.key_lessons = [
1087
- ...rollingSummary.key_lessons,
1324
+ rollingSummary.key_outcomes = [
1325
+ ...rollingSummary.key_outcomes,
1088
1326
  ...lr.wrong_assumptions.map((a) => `Wrong assumption corrected: ${a}`),
1089
1327
  ];
1090
1328
  }
@@ -1208,6 +1446,21 @@ export function compileL2(request, vaultContext) {
1208
1446
  ])];
1209
1447
  loopObjective = updatedLo;
1210
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
+ }
1211
1464
  // ── Route to technique-specific specialist (v1.1 deep integration) ──
1212
1465
  let prompt;
1213
1466
  if (technique === "step-back") {
@@ -1222,6 +1475,17 @@ export function compileL2(request, vaultContext) {
1222
1475
  else {
1223
1476
  prompt = compileGeneric(request, goalId, constraints, loopObjective, rollingText, analysis, referenceFile, technique);
1224
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
+ }
1225
1489
  // ── P2: Emerged subtasks → suggested next task ──
1226
1490
  let suggestedNextTask = "";
1227
1491
  if (request.last_round_result?.emerged_subtasks?.length) {
@@ -1333,7 +1597,7 @@ export function compileL2(request, vaultContext) {
1333
1597
  progressDashboard = lines.join("\n");
1334
1598
  }
1335
1599
  return makeLoopCompileResponse({
1336
- status: "ok",
1600
+ status: AgentStatus.OK,
1337
1601
  prompt: prompt + progressDashboard,
1338
1602
  recompile_level: "l2",
1339
1603
  diff_from_previous: request.round === 1 || request.plan_source
@@ -1345,6 +1609,7 @@ export function compileL2(request, vaultContext) {
1345
1609
  technique_used: technique,
1346
1610
  reference_file: referenceFile,
1347
1611
  rolling_summary: rollingSummary,
1612
+ checkpoint_summary: checkpointSummary,
1348
1613
  loop_id: request.loop_id,
1349
1614
  round: request.round,
1350
1615
  goal_id: goalId,
@@ -1435,10 +1700,8 @@ function compileGeneric(request, goalId, constraints, loopObjective, rollingText
1435
1700
  "### Generation Instructions",
1436
1701
  `1. Read \`${referenceFile}\` — study its structure rules, section count, and format requirements`,
1437
1702
  "2. Generate a complete prompt following that technique's structure",
1438
- "3. Inject all hard constraints and the loop objective into the prompt",
1439
- "4. If Cross-Round Summary is present above, incorporate its recurring issues and key lessons",
1440
- "5. The prompt must be self-contained — ready for a coding agent to execute",
1441
- "6. Output only the generated prompt — no preamble, no meta-commentary",
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",
1442
1705
  ];
1443
1706
  return lines.join("\n");
1444
1707
  }
@@ -1497,19 +1760,12 @@ function compileStepBack(request, goalId, constraints, loopObjective, rollingTex
1497
1760
  "**Section 6 transition sentence (MANDATORY)**:",
1498
1761
  "\"基于上述抽象框架,实现以下所有功能。\"",
1499
1762
  "",
1500
- "### Quality Checklist",
1501
- "- [ ] Frameworks are abstract concepts/formulas/principles, NOT concrete code",
1502
- "- [ ] Section 6 starts with the mandatory transition sentence",
1503
- "- [ ] Abstraction level is consistent across frameworks",
1504
- "- [ ] Step-back question is tightened to minimum necessary generalisation",
1505
- "",
1506
1763
  "### Generation Instructions",
1507
1764
  "1. Extract 2-3 abstract principles/frameworks from the task domain",
1508
1765
  "2. Build ASCII diagrams for each framework (formulas, tables, rules)",
1509
1766
  "3. Apply the abstraction back to the concrete task",
1510
- "4. Inject all hard constraints and the loop objective into the prompt",
1511
- "5. If Cross-Round Summary is present above, incorporate its recurring issues and key lessons",
1512
- "6. Output only the generated prompt — no preamble, no meta-commentary",
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",
1513
1769
  ];
1514
1770
  return lines.join("\n");
1515
1771
  }
@@ -1567,21 +1823,13 @@ function compileLeastToMost(request, goalId, constraints, loopObjective, rolling
1567
1823
  "- Each sub-problem must serve the original task — no unrelated steps",
1568
1824
  "- The final sub-problem must be equivalent to or directly address the original task",
1569
1825
  "",
1570
- "### Quality Checklist",
1571
- "- [ ] Sub-problems ordered simplest → most complex, dependency chain explicit",
1572
- "- [ ] Each sub-problem declares its prerequisite sub-problems",
1573
- "- [ ] Last sub-problem is \"综合实现完整模块\" with integration list",
1574
- "- [ ] Section 6 expands per output format list (NOT per sub-problem)",
1575
- "- [ ] Sub-problem count: 4-6, no more, no less",
1576
- "",
1577
1826
  "### Generation Instructions",
1578
1827
  "1. Decompose the task into 4-6 ordered sub-problems with explicit dependencies",
1579
1828
  "2. Sub-problem 1 starts with the simplest building block (data structures, enums, base config)",
1580
1829
  "3. Each subsequent sub-problem builds on prior results",
1581
1830
  "4. Final sub-problem integrates all components into the complete module",
1582
- "5. Inject all hard constraints and the loop objective into the prompt",
1583
- "6. If Cross-Round Summary is present above, incorporate its recurring issues and key lessons",
1584
- "7. Output only the generated prompt — no preamble, no meta-commentary",
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",
1585
1833
  ];
1586
1834
  return lines.join("\n");
1587
1835
  }
@@ -1660,24 +1908,14 @@ function compileToT(request, goalId, constraints, loopObjective, rollingText, an
1660
1908
  "- thought is a PUBLIC intermediate semantic unit, not hidden chain-of-thought",
1661
1909
  "- State table MUST have: round, branch ID, candidate description, evaluation score/judgment, keep/prune decision",
1662
1910
  "",
1663
- "### Quality Checklist",
1664
- "- [ ] Branch count 2-4, depth ≤3 (prevents token explosion)",
1665
- "- [ ] Evaluation criteria table has ≥3 dimensions including correctness, feasibility, constraint matching",
1666
- "- [ ] State table format explicit (round/branch/candidate/evaluation/decision)",
1667
- "- [ ] Search strategy matches task type (beam/dfs/expert-panel)",
1668
- "- [ ] Hard constraints listed FIRST in evaluation criteria",
1669
- "- [ ] Not \"3 experts casually discussing\" — has explicit branch count, depth, scoring, and pruning rules",
1670
- "- [ ] Ends with model outputting final selection and rationale",
1671
- "",
1672
1911
  "### Generation Instructions",
1673
1912
  "1. Select the appropriate search strategy (beam/dfs/expert-panel) based on task type",
1674
1913
  "2. Define 2-4 candidate approaches as initial branches",
1675
1914
  "3. Build evaluation criteria table with hard constraints ranked first",
1676
1915
  "4. Define the state table format for tracking exploration",
1677
1916
  "5. Set branch count and max depth to prevent token explosion",
1678
- "6. Inject all hard constraints and the loop objective into the prompt",
1679
- "7. If Cross-Round Summary is present above, incorporate its recurring issues and key lessons",
1680
- "8. Output only the generated prompt — no preamble, no meta-commentary",
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",
1681
1919
  ];
1682
1920
  return lines.join("\n");
1683
1921
  }
@@ -1715,7 +1953,9 @@ export function buildSelfEvalBlock(round) {
1715
1953
  ' },',
1716
1954
  ' "retracted_constraints": [],',
1717
1955
  ' "revised_success_criteria": [],',
1718
- ' "wrong_assumptions": []',
1956
+ ' "wrong_assumptions": [],',
1957
+ ' "compression_checkpoint": false,',
1958
+ ' "checkpoint_label": ""',
1719
1959
  "}",
1720
1960
  "---end-loopforge-eval",
1721
1961
  "```",
@@ -1748,6 +1988,15 @@ export function buildSelfEvalBlock(round) {
1748
1988
  `Array of {old, new} objects. Applied to the Loop Objective. Omit or [] if none.`,
1749
1989
  `- wrong_assumptions (P5 — optional): Assumptions from earlier rounds that turned ` +
1750
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.`,
1751
2000
  `- The JSON MUST appear between the ---loopforge-eval and ---end-loopforge-eval markers`,
1752
2001
  `- Do NOT wrap the markers in code fences — output them as raw text`,
1753
2002
  "",
@@ -1772,8 +2021,8 @@ export function compileLoop(request, vaultContext = null) {
1772
2021
  }
1773
2022
  // Layer 2: Compute advisories
1774
2023
  const { warnings, suggestedNextTask, alignment, health } = computeAdvisories(request, vaultContext);
1775
- // Merge advisories into response
1776
- response.warnings = warnings;
2024
+ // Merge advisories into response (preserve P4 consistency warnings from L2)
2025
+ response.warnings = [...(response.warnings || []), ...warnings];
1777
2026
  // Only overwrite suggested_next_task from advisories if the compile function
1778
2027
  // didn't already set one (e.g. L2 sets it from emerged_subtasks)
1779
2028
  if (!response.suggested_next_task) {
@@ -1784,6 +2033,13 @@ export function compileLoop(request, vaultContext = null) {
1784
2033
  response.recompile_level = level;
1785
2034
  // v1.1: Append self-evaluation block for autonomous loop feedback
1786
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
+ }
1787
2043
  return response;
1788
2044
  }
1789
2045
  //# sourceMappingURL=loop-compiler.js.map