loopforge 3.0.0 → 3.2.1

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 (178) hide show
  1. package/README.md +128 -145
  2. package/dist/audit.d.ts +50 -0
  3. package/dist/audit.d.ts.map +1 -0
  4. package/dist/audit.js +158 -0
  5. package/dist/audit.js.map +1 -0
  6. package/dist/canonical-state.d.ts +28 -20
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +82 -173
  9. package/dist/canonical-state.js.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +25 -16
  12. package/dist/cli.js.map +1 -1
  13. package/dist/cognitive-governance.d.ts +25 -33
  14. package/dist/cognitive-governance.d.ts.map +1 -1
  15. package/dist/cognitive-governance.js +38 -138
  16. package/dist/cognitive-governance.js.map +1 -1
  17. package/dist/enforcement-gate.d.ts +76 -0
  18. package/dist/enforcement-gate.d.ts.map +1 -0
  19. package/dist/enforcement-gate.js +1071 -0
  20. package/dist/enforcement-gate.js.map +1 -0
  21. package/dist/engine.d.ts +99 -0
  22. package/dist/engine.d.ts.map +1 -0
  23. package/dist/engine.js +839 -0
  24. package/dist/engine.js.map +1 -0
  25. package/dist/evidence-claims.d.ts +50 -0
  26. package/dist/evidence-claims.d.ts.map +1 -0
  27. package/dist/evidence-claims.js +134 -0
  28. package/dist/evidence-claims.js.map +1 -0
  29. package/dist/evidence-provider.d.ts +21 -23
  30. package/dist/evidence-provider.d.ts.map +1 -1
  31. package/dist/evidence-provider.js +74 -34
  32. package/dist/evidence-provider.js.map +1 -1
  33. package/dist/generate-schema.js +1 -84
  34. package/dist/generate-schema.js.map +1 -1
  35. package/dist/index.d.ts +19 -22
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +16 -15
  38. package/dist/index.js.map +1 -1
  39. package/dist/loop-compiler.d.ts +41 -3
  40. package/dist/loop-compiler.d.ts.map +1 -1
  41. package/dist/loop-compiler.js +632 -273
  42. package/dist/loop-compiler.js.map +1 -1
  43. package/dist/loop-extras-parser.d.ts +2 -2
  44. package/dist/loop-extras-parser.d.ts.map +1 -1
  45. package/dist/loop-extras-parser.js +2 -2
  46. package/dist/loop-extras-parser.js.map +1 -1
  47. package/dist/loop-projection.d.ts +26 -0
  48. package/dist/loop-projection.d.ts.map +1 -0
  49. package/dist/loop-projection.js +142 -0
  50. package/dist/loop-projection.js.map +1 -0
  51. package/dist/loop-store.d.ts +112 -24
  52. package/dist/loop-store.d.ts.map +1 -1
  53. package/dist/loop-store.js +365 -218
  54. package/dist/loop-store.js.map +1 -1
  55. package/dist/mcp/round-lifecycle.d.ts +216 -0
  56. package/dist/mcp/round-lifecycle.d.ts.map +1 -0
  57. package/dist/mcp/round-lifecycle.js +926 -0
  58. package/dist/mcp/round-lifecycle.js.map +1 -0
  59. package/dist/mcp/server.d.ts +2 -1
  60. package/dist/mcp/server.d.ts.map +1 -1
  61. package/dist/mcp/server.js +28 -42
  62. package/dist/mcp/server.js.map +1 -1
  63. package/dist/mcp/session.d.ts +53 -101
  64. package/dist/mcp/session.d.ts.map +1 -1
  65. package/dist/mcp/session.js +306 -1105
  66. package/dist/mcp/session.js.map +1 -1
  67. package/dist/mcp/tools.d.ts +13 -16
  68. package/dist/mcp/tools.d.ts.map +1 -1
  69. package/dist/mcp/tools.js +479 -413
  70. package/dist/mcp/tools.js.map +1 -1
  71. package/dist/policy-metrics.d.ts +12 -17
  72. package/dist/policy-metrics.d.ts.map +1 -1
  73. package/dist/policy-metrics.js +95 -161
  74. package/dist/policy-metrics.js.map +1 -1
  75. package/dist/policy.d.ts +44 -35
  76. package/dist/policy.d.ts.map +1 -1
  77. package/dist/policy.js +31 -115
  78. package/dist/policy.js.map +1 -1
  79. package/dist/prompt-assembler.d.ts +37 -6
  80. package/dist/prompt-assembler.d.ts.map +1 -1
  81. package/dist/prompt-assembler.js +389 -160
  82. package/dist/prompt-assembler.js.map +1 -1
  83. package/dist/prompt-policy.d.ts +1 -8
  84. package/dist/prompt-policy.d.ts.map +1 -1
  85. package/dist/prompt-policy.js +8 -26
  86. package/dist/prompt-policy.js.map +1 -1
  87. package/dist/protocol.d.ts +357 -229
  88. package/dist/protocol.d.ts.map +1 -1
  89. package/dist/protocol.js +139 -15
  90. package/dist/protocol.js.map +1 -1
  91. package/dist/replay.d.ts +5 -13
  92. package/dist/replay.d.ts.map +1 -1
  93. package/dist/replay.js +19 -55
  94. package/dist/replay.js.map +1 -1
  95. package/dist/round-coordinator.d.ts +20 -24
  96. package/dist/round-coordinator.d.ts.map +1 -1
  97. package/dist/round-coordinator.js +99 -92
  98. package/dist/round-coordinator.js.map +1 -1
  99. package/dist/round-driver.d.ts +14 -13
  100. package/dist/round-driver.d.ts.map +1 -1
  101. package/dist/round-driver.js +26 -14
  102. package/dist/round-driver.js.map +1 -1
  103. package/dist/round-transaction.d.ts +15 -18
  104. package/dist/round-transaction.d.ts.map +1 -1
  105. package/dist/round-transaction.js +38 -47
  106. package/dist/round-transaction.js.map +1 -1
  107. package/dist/self-eval.d.ts +53 -0
  108. package/dist/self-eval.d.ts.map +1 -0
  109. package/dist/self-eval.js +284 -0
  110. package/dist/self-eval.js.map +1 -0
  111. package/dist/storage.d.ts +9 -4
  112. package/dist/storage.d.ts.map +1 -1
  113. package/dist/storage.js +18 -6
  114. package/dist/storage.js.map +1 -1
  115. package/dist/token-utils.d.ts +15 -3
  116. package/dist/token-utils.d.ts.map +1 -1
  117. package/dist/token-utils.js +24 -5
  118. package/dist/token-utils.js.map +1 -1
  119. package/dist/verification-gate.d.ts +108 -0
  120. package/dist/verification-gate.d.ts.map +1 -0
  121. package/dist/verification-gate.js +971 -0
  122. package/dist/verification-gate.js.map +1 -0
  123. package/loop_policy.json +44 -15
  124. package/package.json +5 -10
  125. package/skills/perception/SKILL.md +182 -199
  126. package/dist/canonical-event-writer.d.ts +0 -20
  127. package/dist/canonical-event-writer.d.ts.map +0 -1
  128. package/dist/canonical-event-writer.js +0 -131
  129. package/dist/canonical-event-writer.js.map +0 -1
  130. package/dist/compiler-subgoals.d.ts +0 -7
  131. package/dist/compiler-subgoals.d.ts.map +0 -1
  132. package/dist/compiler-subgoals.js +0 -93
  133. package/dist/compiler-subgoals.js.map +0 -1
  134. package/dist/derive-id.d.ts +0 -12
  135. package/dist/derive-id.d.ts.map +0 -1
  136. package/dist/derive-id.js +0 -31
  137. package/dist/derive-id.js.map +0 -1
  138. package/dist/mcp/session-projection.d.ts +0 -5
  139. package/dist/mcp/session-projection.d.ts.map +0 -1
  140. package/dist/mcp/session-projection.js +0 -18
  141. package/dist/mcp/session-projection.js.map +0 -1
  142. package/dist/mcp/session-queue.d.ts +0 -3
  143. package/dist/mcp/session-queue.d.ts.map +0 -1
  144. package/dist/mcp/session-queue.js +0 -18
  145. package/dist/mcp/session-queue.js.map +0 -1
  146. package/dist/mcp/session-types.d.ts +0 -72
  147. package/dist/mcp/session-types.d.ts.map +0 -1
  148. package/dist/mcp/session-types.js +0 -2
  149. package/dist/mcp/session-types.js.map +0 -1
  150. package/dist/round-commit-store.d.ts +0 -12
  151. package/dist/round-commit-store.d.ts.map +0 -1
  152. package/dist/round-commit-store.js +0 -13
  153. package/dist/round-commit-store.js.map +0 -1
  154. package/dist/round-gate.d.ts +0 -31
  155. package/dist/round-gate.d.ts.map +0 -1
  156. package/dist/round-gate.js +0 -259
  157. package/dist/round-gate.js.map +0 -1
  158. package/dist/round-recovery.d.ts +0 -17
  159. package/dist/round-recovery.d.ts.map +0 -1
  160. package/dist/round-recovery.js +0 -117
  161. package/dist/round-recovery.js.map +0 -1
  162. package/dist/round-report.d.ts +0 -16
  163. package/dist/round-report.d.ts.map +0 -1
  164. package/dist/round-report.js +0 -401
  165. package/dist/round-report.js.map +0 -1
  166. package/dist/runtime-services.d.ts +0 -59
  167. package/dist/runtime-services.d.ts.map +0 -1
  168. package/dist/runtime-services.js +0 -460
  169. package/dist/runtime-services.js.map +0 -1
  170. package/dist/vault-format.d.ts +0 -4
  171. package/dist/vault-format.d.ts.map +0 -1
  172. package/dist/vault-format.js +0 -4
  173. package/dist/vault-format.js.map +0 -1
  174. package/dist/vault-types.d.ts +0 -24
  175. package/dist/vault-types.d.ts.map +0 -1
  176. package/dist/vault-types.js +0 -2
  177. package/dist/vault-types.js.map +0 -1
  178. package/loopforge-policy.json +0 -340
package/dist/engine.js ADDED
@@ -0,0 +1,839 @@
1
+ /** LoopForge-loop_compile — Engine (outer loop manager).
2
+ *
3
+ * 2-mode engine with vault-backed loop lineage persistence.
4
+ * invokeLoopCompile (primary), invokeFeedback.
5
+ * Circuit breaker prevents infinite stall loops.
6
+ * EngineMetrics tracks silent-failure counters for observability.
7
+ */
8
+ import { randomUUID } from "node:crypto";
9
+ import { getPolicy } from "./policy.js";
10
+ import { FileLoopStore, queryLoopEntries } from "./loop-store.js";
11
+ import { AgentStatus, Mode, makeExecutionFeedback, makeLoopCompileRequest, makeLoopObjective, makeLoopRoundResult, makeSessionState, makeTaskId, } from "./protocol.js";
12
+ import { compileLoop } from "./loop-compiler.js";
13
+ import { logEvent } from "./observability.js";
14
+ import { isRecord } from "./token-utils.js";
15
+ import { isProcessResult, parseRoundTransactionSnapshot } from "./round-transaction.js";
16
+ import { policyMetrics } from "./policy-metrics.js";
17
+ import { parseExecutionEvidence, parseCriterionRevisions, parseWorkerResults, } from "./self-eval.js";
18
+ import { parseLoopExtras } from "./loop-extras-parser.js";
19
+ // ── Re-export self-evaluation utilities (moved to self-eval.ts) ──────────
20
+ export { parseExecutionEvidence, parseCriterionRevisions, parseWorkerResults, extractSelfEvaluation, buildSelfEvaluation, } from "./self-eval.js";
21
+ function makeEngineMetrics() {
22
+ return {
23
+ vaultWriteErrors: 0,
24
+ sessionStart: Date.now(),
25
+ };
26
+ }
27
+ // ═══════════════════════════════════════════════════════════════════════════
28
+ // LoopForgeEngine
29
+ // ═══════════════════════════════════════════════════════════════════════════
30
+ export class LoopForgeEngine {
31
+ state = null;
32
+ store = null;
33
+ metrics = null;
34
+ lastTask = null;
35
+ constructor(store) {
36
+ this.store = store ?? null;
37
+ }
38
+ resolveStore() {
39
+ if (this.store === null) {
40
+ this.store = new FileLoopStore(getPolicy().backend.root_dir);
41
+ }
42
+ return this.store;
43
+ }
44
+ /** Public accessor for the loop store — used by the round transaction
45
+ * coordinator and round driver. */
46
+ getStore() {
47
+ return this.resolveStore();
48
+ }
49
+ /** Expose engine health counters for observability (MCP status, logging). */
50
+ getMetrics() {
51
+ if (this.metrics === null) {
52
+ this.metrics = makeEngineMetrics();
53
+ }
54
+ return this.metrics;
55
+ }
56
+ ensureInit(request) {
57
+ if (this.state === null) {
58
+ this.state = makeSessionState(request.task_id || makeTaskId(request.task));
59
+ }
60
+ if (this.metrics === null) {
61
+ this.metrics = makeEngineMetrics();
62
+ }
63
+ }
64
+ // ═══════════════════════════════════════════════════════════════════════
65
+ // Feedback persistence
66
+ // ═══════════════════════════════════════════════════════════════════════
67
+ persistFeedbackToVault(signal) {
68
+ if (this.metrics === null) {
69
+ this.metrics = makeEngineMetrics();
70
+ }
71
+ // v2.6: write directly — the buffer was always flushed immediately
72
+ // (autoFeedback calls persistFeedbackToVault then immediately flushFeedbackBuffer).
73
+ const entry = this.buildFeedbackEntry(signal);
74
+ if (!entry)
75
+ return false;
76
+ try {
77
+ this.resolveStore().appendEntry(entry);
78
+ return true;
79
+ }
80
+ catch (error) {
81
+ if (this.metrics)
82
+ this.metrics.vaultWriteErrors++;
83
+ console.warn(`LoopForge: vault write failed (feedback_persist) — ${error}`);
84
+ logEvent("vault_write_error", { error: "feedback_persist" });
85
+ policyMetrics.recordVaultWriteError("feedback_persist");
86
+ return false;
87
+ }
88
+ }
89
+ buildFeedbackEntry(signal) {
90
+ try {
91
+ return {
92
+ id: randomUUID(),
93
+ task_id: signal.task_id ?? "feedback",
94
+ version_tag: "v1",
95
+ is_active: true,
96
+ timestamp: new Date().toISOString().replace(/\.\d+Z$/, ""),
97
+ user_intent: String(signal.task_type ?? "").slice(0, 200),
98
+ success: signal.success ?? false,
99
+ execution_feedback: JSON.stringify({
100
+ success: signal.success ?? false,
101
+ status: signal.success ? "success" : "partial",
102
+ constraint_compliance: {
103
+ all_hard_constraints_met: !Array.isArray(signal.violations) || signal.violations.length === 0,
104
+ violations: signal.violations ?? [],
105
+ },
106
+ output_summary: signal.task_type ?? "",
107
+ improvement_notes: signal.manual_fixes ?? "",
108
+ }),
109
+ task_type: signal.task_type ?? "",
110
+ tags: signal.skill_used ? [signal.skill_used] : [],
111
+ skill_used: signal.skill_used ?? "",
112
+ loop_id: signal.loop_id,
113
+ loop_lineage: signal.loop_lineage ?? {},
114
+ // Persist execution_evidence so the enforcement gate (R4/R5) can
115
+ // read progress_estimate from vault entries across rounds.
116
+ execution_evidence: signal.execution_evidence ?? null,
117
+ discovered_constraints: signal.discovered_constraints ?? [],
118
+ emerged_subtasks: signal.emerged_subtasks ?? [],
119
+ // v3.2.1: persisted so the subgoal_drift check can read prior
120
+ // rounds' completed/canceled sub-goals from committed entries.
121
+ completed_subtasks: signal.completed_subtasks ?? [],
122
+ blocked_subtasks: signal.blocked_subtasks ?? [],
123
+ canceled_subtasks: signal.canceled_subtasks ?? [],
124
+ retracted_constraints: signal.retracted_constraints ?? [],
125
+ worker_results: signal.worker_results ?? [],
126
+ };
127
+ }
128
+ catch {
129
+ if (this.metrics)
130
+ this.metrics.vaultWriteErrors++;
131
+ logEvent("vault_write_error", { error: "feedback_entry_build" });
132
+ return null;
133
+ }
134
+ }
135
+ // ═══════════════════════════════════════════════════════════════════════
136
+ // Lineage persistence
137
+ // ═══════════════════════════════════════════════════════════════════════
138
+ persistLoopLineage(response, request) {
139
+ if (this.metrics === null)
140
+ this.metrics = makeEngineMetrics();
141
+ const loopObjDict = response.loop_objective
142
+ ? response.loop_objective
143
+ : null;
144
+ const structuredLineage = {
145
+ loop_id: response.loop_id,
146
+ round: response.round,
147
+ goal_id: response.goal_id,
148
+ goal_text_hash: response.goal_text_hash,
149
+ recompile_level: response.recompile_level,
150
+ constraints_active: response.constraints_active,
151
+ task: request.task,
152
+ success: true,
153
+ };
154
+ // v3.2: Persist what this round's prompt actually presented (L1 only —
155
+ // L0/L2 compiles leave the artifact field undefined). The next L1 compile
156
+ // reads these as its collapse diff baseline. Field extension of the
157
+ // existing lineage entry — no new persistence format.
158
+ const presented = response.prompt_artifact?.presentedState;
159
+ if (presented) {
160
+ structuredLineage.presented_constraint_ids = presented.constraintIds;
161
+ structuredLineage.presented_subgoals = presented.subGoals;
162
+ structuredLineage.presented_milestone_ranges = presented.milestoneRanges;
163
+ }
164
+ let lastOutputSummary = "";
165
+ let lastViolations = [];
166
+ if (request.last_round_result) {
167
+ lastOutputSummary = request.last_round_result.output_summary || "";
168
+ lastViolations = request.last_round_result.constraint_violations || [];
169
+ }
170
+ const entry = {
171
+ id: randomUUID(),
172
+ task_id: `loop:${response.loop_id}:r${response.round}`,
173
+ version_tag: "v1",
174
+ is_active: true,
175
+ timestamp: new Date().toISOString().replace(/\.\d+Z$/, ""),
176
+ user_intent: `loop_compile round ${response.round} — ${response.goal_id}`,
177
+ task_type: "loop_lineage",
178
+ loop_id: response.loop_id,
179
+ loop_lineage: structuredLineage,
180
+ loop_objective: loopObjDict,
181
+ task: request.task,
182
+ output_summary: lastOutputSummary,
183
+ constraint_violations: lastViolations,
184
+ tags: [response.loop_id, response.recompile_level, response.goal_id],
185
+ };
186
+ // 1. JSON vault write (primary)
187
+ let vaultOk = false;
188
+ try {
189
+ this.resolveStore().appendEntry(entry);
190
+ vaultOk = true;
191
+ }
192
+ catch (error) {
193
+ if (this.metrics)
194
+ this.metrics.vaultWriteErrors++;
195
+ console.warn(`LoopForge: vault write failed (lineage_persist) — ${error}`);
196
+ logEvent("vault_write_error", { error: "persist_lineage_json" });
197
+ policyMetrics.recordVaultWriteError("lineage_persist");
198
+ }
199
+ return vaultOk;
200
+ }
201
+ // ═══════════════════════════════════════════════════════════════════════
202
+ // Delegation journal (v1.9 — AgentTool mode)
203
+ // ═══════════════════════════════════════════════════════════════════════
204
+ /** Record sub-agent delegations for this round into the vault.
205
+ * Written as a lightweight journal entry so the main agent's rolling
206
+ * summary can reference delegation history in subsequent rounds. */
207
+ recordDelegation(loopId, round, entries) {
208
+ if (!entries.length)
209
+ return;
210
+ const taskId = `loop:${loopId}:r${round}:delegations`;
211
+ const existing = queryLoopEntries(this.resolveStore(), loopId, { prefix: taskId })
212
+ .some((candidate) => candidate.task_id === taskId);
213
+ if (existing)
214
+ return;
215
+ const entry = {
216
+ id: randomUUID(),
217
+ task_id: taskId,
218
+ version_tag: "v1",
219
+ is_active: true,
220
+ timestamp: new Date().toISOString().replace(/\.\d+Z$/, ""),
221
+ user_intent: `Delegation journal — round ${round}`,
222
+ task_type: "delegation_journal",
223
+ loop_id: loopId,
224
+ loop_lineage: {
225
+ round,
226
+ delegations: entries.map((e) => ({
227
+ index: e.index,
228
+ agentId: e.agentId,
229
+ subAgentType: e.subAgentType,
230
+ subTask: e.subTask,
231
+ resultSummary: e.resultSummary,
232
+ success: e.success,
233
+ outcome: e.outcome ?? (e.success ? "success" : "failed"),
234
+ discoveredConstraints: e.discoveredConstraints,
235
+ })),
236
+ },
237
+ };
238
+ try {
239
+ this.resolveStore().appendEntry(entry);
240
+ }
241
+ catch (error) {
242
+ if (this.metrics)
243
+ this.metrics.vaultWriteErrors++;
244
+ console.warn(`LoopForge: vault write failed (delegation_persist) — ${error}`);
245
+ logEvent("vault_write_error", { error: "record_delegation" });
246
+ policyMetrics.recordVaultWriteError("delegation_persist");
247
+ }
248
+ }
249
+ // ═══════════════════════════════════════════════════════════════════════
250
+ // Hydrate loop context from vault
251
+ // ═══════════════════════════════════════════════════════════════════════
252
+ /** Eval-owned fields merged from the committed round decision onto the
253
+ * lineage entry. These are the agent's report of what happened during the
254
+ * round; compile-time lineage fields (constraints_active, goal_text_hash,
255
+ * recompile_level, …) are left untouched. */
256
+ static EVAL_MERGED_LINEAGE_FIELDS = [
257
+ "execution_evidence",
258
+ "compression_checkpoint",
259
+ "checkpoint_label",
260
+ "discovered_constraints",
261
+ "emerged_subtasks",
262
+ "retracted_constraints",
263
+ "wrong_assumptions",
264
+ "completed_subtasks",
265
+ "blocked_subtasks",
266
+ "canceled_subtasks",
267
+ "next_action",
268
+ "objective_refinement",
269
+ "outcome",
270
+ "blocker",
271
+ "drift_clarification",
272
+ "revised_success_criteria",
273
+ "worker_results",
274
+ "prompt_requests",
275
+ ];
276
+ /** Merge the committed round decision (feedback entry) into the compile-time
277
+ * lineage entry so the compiler and projections see the full per-round
278
+ * truth: verification flags, round success, and the agent's own evaluation. */
279
+ mergeCommittedRound(entry, lineage, fb) {
280
+ const fbLineage = isRecord(fb.loop_lineage) ? fb.loop_lineage : null;
281
+ const rt = fbLineage && isRecord(fbLineage.round_transaction)
282
+ ? fbLineage.round_transaction
283
+ : null;
284
+ const result = rt && isProcessResult(rt.result) ? rt.result : null;
285
+ const snapshot = rt ? parseRoundTransactionSnapshot(rt.snapshot) : null;
286
+ const evaluation = snapshot && isRecord(snapshot.evaluation)
287
+ ? snapshot.evaluation
288
+ : null;
289
+ if (result) {
290
+ if (Array.isArray(result.verificationFlags) &&
291
+ result.verificationFlags.length > 0) {
292
+ entry.verification_flags = result.verificationFlags;
293
+ }
294
+ if (typeof result.roundSuccess === "boolean") {
295
+ entry.success = result.roundSuccess;
296
+ lineage.success = result.roundSuccess;
297
+ }
298
+ // The committed decision's action — lets the compiler detect a
299
+ // post-backtrack recovery boundary (L2 rehydration) without a
300
+ // second persistence format.
301
+ lineage.committed_action = result.action;
302
+ }
303
+ if (!evaluation)
304
+ return;
305
+ for (const key of LoopForgeEngine.EVAL_MERGED_LINEAGE_FIELDS) {
306
+ const value = evaluation[key];
307
+ if (value === undefined)
308
+ continue;
309
+ lineage[key] = value;
310
+ entry[key] = value;
311
+ }
312
+ if (typeof evaluation.success === "boolean") {
313
+ entry.success = evaluation.success;
314
+ lineage.success = evaluation.success;
315
+ }
316
+ if (typeof evaluation.output_summary === "string" && evaluation.output_summary) {
317
+ entry.output_summary = evaluation.output_summary;
318
+ }
319
+ if (Array.isArray(evaluation.constraint_violations)) {
320
+ entry.constraint_violations = evaluation.constraint_violations;
321
+ }
322
+ }
323
+ /** v3.0.1: Per-engine hydration cache. The vault is append-only per round —
324
+ * between two compiles of one loop only the newest round(s) changed. The
325
+ * cache holds the merged lineage entries (the exact shape the compiler
326
+ * consumes; feedback bodies are merged in, never returned), so a long-lived
327
+ * process compiles incrementally: one round document read per round
328
+ * boundary instead of the full history. Nothing is persisted; a fresh
329
+ * engine hydrates fully once. `coveredRound` is the highest CONTIGUOUS
330
+ * COMMITTED round (feedback merged) whose entries are in the cache — the
331
+ * compiler only reads rounds below the compile round, so
332
+ * `coveredRound >= round - 1` means the cache is complete for that compile.
333
+ * Lineage-only entries of not-yet-committed rounds may trail the cache but
334
+ * never advance coveredRound (a later read replaces them post-commit). */
335
+ hydrationCache = null;
336
+ /** The round a feedback entry belongs to, or null. */
337
+ static feedbackRound(taskId) {
338
+ const parts = taskId.split(":r");
339
+ if (parts.length < 2)
340
+ return null;
341
+ const roundStr = parts[1].split(":")[0];
342
+ const round = parseInt(roundStr, 10);
343
+ return Number.isNaN(round) ? null : round;
344
+ }
345
+ /** Highest contiguous round (from 1) present in the given feedback entries. */
346
+ static contiguousCommittedRound(fbEntries) {
347
+ const committed = new Set();
348
+ for (const fe of fbEntries) {
349
+ const round = LoopForgeEngine.feedbackRound(String(fe.task_id ?? ""));
350
+ if (round !== null)
351
+ committed.add(round);
352
+ }
353
+ let covered = 0;
354
+ while (committed.has(covered + 1))
355
+ covered++;
356
+ return covered;
357
+ }
358
+ /** Merge committed feedback entries into lineage entries and apply the
359
+ * legacy output_summary / constraint_violations backfill. Shared by the
360
+ * full and incremental hydration paths so both produce identical shapes.
361
+ *
362
+ * The round commit entry (`loop:<id>:r<N>:feedback`) carries the full
363
+ * committed decision — verification flags, round success, and the agent's
364
+ * SelfEvaluation — which the compiler needs to derive milestones,
365
+ * constraint decay, sub-goal accumulation, and rolling summaries. Without
366
+ * this merge the cross-round features only see the 8 compile-time lineage
367
+ * fields. */
368
+ buildMergedEntries(fresh, fbFresh) {
369
+ const fbByRound = new Map();
370
+ for (const fe of fbFresh) {
371
+ const fbRound = LoopForgeEngine.feedbackRound(String(fe.task_id ?? ""));
372
+ if (fbRound !== null)
373
+ fbByRound.set(fbRound, fe);
374
+ }
375
+ for (const entry of fresh) {
376
+ const lineage = (entry.loop_lineage ?? entry.lineage ?? {});
377
+ const rnd = lineage.round;
378
+ const fb = rnd ? fbByRound.get(rnd) : undefined;
379
+ if (fb)
380
+ this.mergeCommittedRound(entry, lineage, fb);
381
+ if (!entry.output_summary) {
382
+ entry.output_summary = lineage.output_summary ?? "";
383
+ }
384
+ if (!entry.constraint_violations) {
385
+ entry.constraint_violations =
386
+ lineage.constraint_violations ?? [];
387
+ }
388
+ }
389
+ return fresh;
390
+ }
391
+ hydrateLoopContext(loopId, round) {
392
+ const store = this.resolveStore();
393
+ const prefix = `loop:${loopId}:r`;
394
+ // v3.0.1: cached hydration — fast path when the cache already covers the
395
+ // requested round (same-round retries), incremental read of only the
396
+ // rounds committed since the cache point otherwise.
397
+ if (round !== undefined && this.hydrationCache?.loopId === loopId) {
398
+ const cache = this.hydrationCache;
399
+ if (cache.coveredRound >= round - 1) {
400
+ return { results: cache.entries, global_entries: [] };
401
+ }
402
+ const sinceRound = cache.coveredRound + 1;
403
+ const fresh = queryLoopEntries(store, loopId, { prefix, sinceRound });
404
+ const freshFb = queryLoopEntries(store, loopId, {
405
+ prefix,
406
+ sinceRound,
407
+ feedbackOnly: true,
408
+ });
409
+ const merged = this.buildMergedEntries(fresh, freshFb);
410
+ for (const entry of merged) {
411
+ const tid = String(entry.task_id ?? "");
412
+ // Replace-by-task_id: a round read earlier as a lineage-only entry
413
+ // (not yet committed) must be replaced by its post-commit merged
414
+ // version — a stale unmerged entry would silently drop the round's
415
+ // committed decision (verification flags, committed_action, eval).
416
+ const index = cache.entries.findIndex((e) => String(e.task_id ?? "") === tid);
417
+ if (index >= 0)
418
+ cache.entries[index] = entry;
419
+ else
420
+ cache.entries.push(entry);
421
+ }
422
+ // Advance coveredRound only for rounds that actually committed since
423
+ // the cache point — the fresh reads start at coveredRound + 1, so the
424
+ // contiguous run extends from there.
425
+ const freshCommitted = new Set();
426
+ for (const fe of freshFb) {
427
+ const r = LoopForgeEngine.feedbackRound(String(fe.task_id ?? ""));
428
+ if (r !== null)
429
+ freshCommitted.add(r);
430
+ }
431
+ while (freshCommitted.has(cache.coveredRound + 1))
432
+ cache.coveredRound++;
433
+ return { results: cache.entries, global_entries: [] };
434
+ }
435
+ // Full path — cold engine, different loop, or a round-less hydrate (e.g.
436
+ // health checks). Merge all feedback entries, dedupe by task_id (round
437
+ // documents hold the latest write per key), and cache the merged view.
438
+ // coveredRound counts only contiguous COMMITTED rounds — lineage-only
439
+ // entries of uncommitted rounds may trail the cache but must not make the
440
+ // fast path serve unmerged state.
441
+ const results = queryLoopEntries(store, loopId, { prefix });
442
+ const fbEntries = queryLoopEntries(store, loopId, { prefix, feedbackOnly: true });
443
+ const merged = this.buildMergedEntries(results, fbEntries);
444
+ if (!merged.length)
445
+ return null;
446
+ const byTaskId = new Map();
447
+ for (const entry of merged)
448
+ byTaskId.set(String(entry.task_id ?? ""), entry);
449
+ const entries = [...byTaskId.values()];
450
+ this.hydrationCache = {
451
+ loopId,
452
+ coveredRound: LoopForgeEngine.contiguousCommittedRound(fbEntries),
453
+ entries,
454
+ };
455
+ return { results: entries, global_entries: [] };
456
+ }
457
+ // ═══════════════════════════════════════════════════════════════════════
458
+ // Feedback (public)
459
+ // ═══════════════════════════════════════════════════════════════════════
460
+ invokeFeedback(request, _hydrateResults) {
461
+ this.ensureInit(request);
462
+ this.lastTask = request.task;
463
+ const fb = request.feedback;
464
+ if (!fb) {
465
+ return {
466
+ status: AgentStatus.ERROR,
467
+ response: {
468
+ status: AgentStatus.ERROR,
469
+ prompt: null,
470
+ error: "Feedback mode requires a feedback payload.",
471
+ },
472
+ };
473
+ }
474
+ const success = fb.success;
475
+ const violations = fb.constraint_violations ?? [];
476
+ const fixes = fb.manual_fixes_needed ?? "";
477
+ // Loop-aware task_id for feedback→lineage backfill
478
+ const loopId = request.loop_id;
479
+ const loopRound = request.round;
480
+ // v2.14: feedback entries are loop-scoped — a request without loop_id
481
+ // and round can never be persisted. Fail loudly instead of reporting
482
+ // "Feedback Recorded" for a write that was silently dropped.
483
+ if (!loopId || loopRound === undefined) {
484
+ return {
485
+ status: AgentStatus.ERROR,
486
+ response: {
487
+ status: AgentStatus.ERROR,
488
+ prompt: null,
489
+ error: "Feedback requires loop_id and round — feedback entries are loop-scoped.",
490
+ },
491
+ };
492
+ }
493
+ const taskId = `loop:${loopId}:r${loopRound}:feedback`;
494
+ const signal = {
495
+ task_id: taskId,
496
+ task_type: request.task.slice(0, 80),
497
+ success,
498
+ skill_used: request.skill_name ?? "",
499
+ violations,
500
+ manual_fixes: fixes,
501
+ loop_id: loopId,
502
+ round: loopRound,
503
+ };
504
+ const persisted = this.persistFeedbackToVault(signal);
505
+ if (!persisted) {
506
+ return {
507
+ status: AgentStatus.ERROR,
508
+ response: {
509
+ status: AgentStatus.ERROR,
510
+ prompt: null,
511
+ error: "Feedback could not be persisted to the loop store — the round was not recorded.",
512
+ },
513
+ };
514
+ }
515
+ // Update state
516
+ this.state.call_count++;
517
+ this.state.success_trend.push(success);
518
+ if (this.state.success_trend.length > 20) {
519
+ this.state.success_trend = this.state.success_trend.slice(-20);
520
+ }
521
+ logEvent("round_complete", {
522
+ success,
523
+ loopId: loopId ?? "unknown",
524
+ round: loopRound ?? this.state.call_count,
525
+ });
526
+ return {
527
+ status: AgentStatus.OK,
528
+ response: {
529
+ status: AgentStatus.OK,
530
+ prompt: `## Feedback Recorded\n\nSuccess: ${success}\nSignals: 1`,
531
+ error: null,
532
+ },
533
+ };
534
+ }
535
+ // ═══════════════════════════════════════════════════════════════════════
536
+ // Auto-Feedback (v1.1 — autonomous loop, no human in the loop)
537
+ // ═══════════════════════════════════════════════════════════════════════
538
+ /** Record self-evaluation from agent output without human intervention.
539
+ * Converts SelfEvaluation → ExecutionFeedback → vault persistence.
540
+ * P0–P2: Also persists discovered_constraints, objective_refinement,
541
+ * and emerged_subtasks for the compiler to consume next round.
542
+ * Call this BEFORE invokeLoopCompile for the next round so that
543
+ * hydrateLoopContext picks up the latest success flags. */
544
+ autoFeedback(selfEval, loopId, round, task, roundTransaction) {
545
+ this.ensureInit({ task, mode: Mode.FEEDBACK, feedback: null, skill_name: null, task_id: null });
546
+ const fb = makeExecutionFeedback({
547
+ output: selfEval.output_summary,
548
+ success: selfEval.success,
549
+ constraint_violations: selfEval.constraint_violations,
550
+ manual_fixes_needed: "",
551
+ });
552
+ const taskId = `loop:${loopId}:r${round}:feedback`;
553
+ const transactionId = typeof roundTransaction?.round_id === "string"
554
+ ? roundTransaction.round_id
555
+ : null;
556
+ const transactionPersisted = (filter) => {
557
+ if (!transactionId)
558
+ return false;
559
+ return queryLoopEntries(this.resolveStore(), loopId, {
560
+ prefix: taskId,
561
+ feedbackOnly: true,
562
+ }).some((entry) => {
563
+ if (entry.task_id !== taskId)
564
+ return false;
565
+ if (filter && !filter(entry))
566
+ return false;
567
+ const lineage = entry.loop_lineage;
568
+ const transaction = lineage?.round_transaction;
569
+ return transaction !== null &&
570
+ typeof transaction === "object" &&
571
+ !Array.isArray(transaction) &&
572
+ transaction.round_id === transactionId;
573
+ });
574
+ };
575
+ // v3.2.1: a committed backtrack decision is a roll-back directive, not
576
+ // the round's outcome. The redo submission reuses the same roundId and
577
+ // must REPLACE the backtrack entry in the vault — otherwise the stale
578
+ // entry keeps poisoning the R4/R5 progress window and the redo's work
579
+ // never lands (it is replayed or its data is ignored forever).
580
+ const alreadyCommitted = transactionPersisted((entry) => {
581
+ const transaction = isRecord(entry.loop_lineage)
582
+ ? entry.loop_lineage.round_transaction
583
+ : null;
584
+ const result = isRecord(transaction) ? transaction.result : null;
585
+ return !(isRecord(result) && result.action === "backtrack");
586
+ });
587
+ // Multi-agent: Merge sub-agent discovered constraints into the main constraint flow
588
+ const subDiscovered = (selfEval.worker_results ?? [])
589
+ .flatMap((w) => w.discoveredConstraints ?? [])
590
+ .filter((c) => c.length > 0);
591
+ const mergedDiscovered = [
592
+ ...new Set([...(selfEval.discovered_constraints ?? []), ...subDiscovered]),
593
+ ];
594
+ const signal = {
595
+ task_id: taskId,
596
+ task_type: task.slice(0, 80),
597
+ success: fb.success,
598
+ skill_used: "",
599
+ violations: selfEval.constraint_violations,
600
+ manual_fixes: "",
601
+ loop_id: loopId,
602
+ round,
603
+ // P0–P2: Evolution fields
604
+ discovered_constraints: mergedDiscovered,
605
+ objective_refinement: selfEval.objective_refinement ?? "",
606
+ emerged_subtasks: selfEval.emerged_subtasks ?? [],
607
+ // v3.2.1: persist the sub-goal lifecycle fields — the subgoal_drift
608
+ // check reads them from committed feedback entries; before this they
609
+ // were never written, so prior rounds' completed/canceled sub-goals
610
+ // were invisible and the pending set grew stale.
611
+ completed_subtasks: selfEval.completed_subtasks ?? [],
612
+ blocked_subtasks: selfEval.blocked_subtasks ?? [],
613
+ canceled_subtasks: selfEval.canceled_subtasks ?? [],
614
+ // P4: Execution evidence
615
+ execution_evidence: selfEval.execution_evidence ?? null,
616
+ // P5: Self-correction
617
+ retracted_constraints: selfEval.retracted_constraints ?? [],
618
+ revised_success_criteria: selfEval.revised_success_criteria ?? [],
619
+ wrong_assumptions: selfEval.wrong_assumptions ?? [],
620
+ // Multi-agent: Worker delegation results
621
+ worker_results: selfEval.worker_results ?? [],
622
+ // v2.12: Tri-state outcome (audit data source)
623
+ outcome: selfEval.outcome,
624
+ loop_lineage: roundTransaction
625
+ ? {
626
+ round,
627
+ round_id: roundTransaction.round_id,
628
+ round_transaction: roundTransaction,
629
+ }
630
+ : {},
631
+ };
632
+ if (!alreadyCommitted) {
633
+ this.persistFeedbackToVault(signal);
634
+ if (transactionId && !transactionPersisted()) {
635
+ throw new Error(`Round feedback commit failed: ${transactionId}`);
636
+ }
637
+ }
638
+ // Multi-agent: Auto-record delegation journal from worker_results
639
+ if (selfEval.worker_results && selfEval.worker_results.length > 0) {
640
+ const entries = selfEval.worker_results.map((w, i) => ({
641
+ index: i + 1,
642
+ agentId: w.agentId,
643
+ subAgentType: w.subAgentType,
644
+ subTask: w.subTask,
645
+ resultSummary: w.resultSummary,
646
+ success: w.success,
647
+ outcome: w.outcome,
648
+ discoveredConstraints: w.discoveredConstraints ?? [],
649
+ }));
650
+ this.recordDelegation(loopId, round, entries);
651
+ }
652
+ // Replaying a committed transaction may repair derived delegation data,
653
+ // but must never apply the feedback to mutable engine state twice.
654
+ if (alreadyCommitted)
655
+ return fb.success;
656
+ // Update state
657
+ this.state.call_count++;
658
+ this.state.success_trend.push(fb.success);
659
+ if (this.state.success_trend.length > 20) {
660
+ this.state.success_trend = this.state.success_trend.slice(-20);
661
+ }
662
+ // v2.5: Old binary-success circuit breaker removed — stalled loops
663
+ // are now detected by the enforcement gate (R4/R5) using
664
+ // progress_estimate gradients rather than success=true/false count.
665
+ logEvent("round_complete", {
666
+ loopId,
667
+ round,
668
+ success: fb.success,
669
+ });
670
+ return fb.success;
671
+ }
672
+ // ═══════════════════════════════════════════════════════════════════════
673
+ // Loop Compile (public, primary)
674
+ // ═══════════════════════════════════════════════════════════════════════
675
+ invokeLoopCompile(request, hydrateResults, options = {}) {
676
+ this.ensureInit(request);
677
+ this.lastTask = request.task;
678
+ // Parse extras via typed extraction pipeline (loop-extras-parser.ts).
679
+ // Errors are collected but never thrown — the compiler always gets
680
+ // best-effort defaults so a malformed request doesn't crash the engine.
681
+ const extras = request;
682
+ const { parsed, ctx } = parseLoopExtras(extras, request.task_id ?? "");
683
+ if (ctx.errors.length > 0) {
684
+ logEvent("extras_parse_errors", {
685
+ errors: ctx.errors.map((e) => `${e.field}: ${e.message}`),
686
+ });
687
+ }
688
+ const lcr = makeLoopCompileRequest({
689
+ loop_id: parsed.loop_id,
690
+ round: parsed.round,
691
+ goal_id: parsed.goal_id,
692
+ task: request.task,
693
+ domain: parsed.domain,
694
+ next_task_proposal: parsed.next_task_proposal,
695
+ plan_source: parsed.plan_source,
696
+ constraints_from_plan: parsed.constraints_from_plan,
697
+ new_since_last_round: parsed.new_since_last_round,
698
+ force_level: parsed.force_level,
699
+ health_check_interval: parsed.health_check_interval,
700
+ external_context: parsed.external_context,
701
+ max_rounds: parsed.max_rounds,
702
+ verification_flags: parsed.verification_flags,
703
+ attempt: parsed.attempt,
704
+ consecutive_rejections: parsed.consecutive_rejections,
705
+ rejection_notice: parsed.rejection_notice,
706
+ });
707
+ // Convert last_round_result if present (object already validated by parser)
708
+ if (parsed.last_round_result) {
709
+ const rr = parsed.last_round_result;
710
+ // Parse P4 execution evidence (shared helper)
711
+ const executionEvidence = parseExecutionEvidence(rr.execution_evidence);
712
+ // Parse P5 revised_success_criteria (shared parser)
713
+ const revisedCriteria = parseCriterionRevisions(rr.revised_success_criteria);
714
+ lcr.last_round_result = makeLoopRoundResult({
715
+ round: rr.round ?? 0,
716
+ success: rr.success ?? false,
717
+ output_summary: rr.output_summary ?? "",
718
+ constraint_violations: rr.constraint_violations ?? [],
719
+ manual_fixes_needed: rr.manual_fixes_needed ?? "",
720
+ // P0–P2: Cognitive evolution fields
721
+ discovered_constraints: Array.isArray(rr.discovered_constraints)
722
+ ? rr.discovered_constraints.filter((v) => typeof v === "string")
723
+ : [],
724
+ objective_refinement: typeof rr.objective_refinement === "string"
725
+ ? rr.objective_refinement
726
+ : "",
727
+ emerged_subtasks: Array.isArray(rr.emerged_subtasks)
728
+ ? rr.emerged_subtasks.filter((v) => typeof v === "string")
729
+ : [],
730
+ // P4: Execution evidence
731
+ execution_evidence: executionEvidence,
732
+ // P5: Self-correction
733
+ retracted_constraints: Array.isArray(rr.retracted_constraints)
734
+ ? rr.retracted_constraints.filter((v) => typeof v === "string")
735
+ : [],
736
+ revised_success_criteria: revisedCriteria,
737
+ wrong_assumptions: Array.isArray(rr.wrong_assumptions)
738
+ ? rr.wrong_assumptions.filter((v) => typeof v === "string")
739
+ : [],
740
+ // v1.10: Checkpoint boundary
741
+ compression_checkpoint: typeof rr.compression_checkpoint === "boolean" ? rr.compression_checkpoint : false,
742
+ checkpoint_label: typeof rr.checkpoint_label === "string" ? rr.checkpoint_label : "",
743
+ // v2.2: Sub-goal lifecycle
744
+ completed_subtasks: Array.isArray(rr.completed_subtasks)
745
+ ? rr.completed_subtasks.filter((v) => typeof v === "string")
746
+ : [],
747
+ blocked_subtasks: Array.isArray(rr.blocked_subtasks)
748
+ ? rr.blocked_subtasks.filter((v) => typeof v === "string")
749
+ : [],
750
+ canceled_subtasks: Array.isArray(rr.canceled_subtasks)
751
+ ? rr.canceled_subtasks.filter((v) => typeof v === "string")
752
+ : [],
753
+ // v2.8: Drift clarification
754
+ drift_clarification: typeof rr.drift_clarification === "string"
755
+ ? rr.drift_clarification
756
+ : undefined,
757
+ // Multi-agent: Worker delegation results (shared parser)
758
+ worker_results: parseWorkerResults(rr.worker_results),
759
+ // v2.12: Tri-state outcome + blocker + retroactive claims
760
+ outcome: rr.outcome === "success" || rr.outcome === "partial" ||
761
+ rr.outcome === "failed" || rr.outcome === "blocked"
762
+ ? rr.outcome
763
+ : undefined,
764
+ blocker: typeof rr.blocker === "string" && rr.blocker.trim().length > 0
765
+ ? rr.blocker.slice(0, 500)
766
+ : undefined,
767
+ retroactiveClaims: Array.isArray(rr.retroactiveClaims)
768
+ ? rr.retroactiveClaims
769
+ .filter((item) => isRecord(item) &&
770
+ typeof item.round === "number" && item.round >= 1 &&
771
+ typeof item.claim === "string" && item.claim.length > 0)
772
+ .map((item) => ({ round: item.round, claim: item.claim.slice(0, 500) }))
773
+ .slice(0, 20)
774
+ : [],
775
+ });
776
+ }
777
+ // Convert loop_objective if present (object already validated by parser)
778
+ if (parsed.loop_objective) {
779
+ const obj = parsed.loop_objective;
780
+ lcr.loop_objective = makeLoopObjective({
781
+ objective: obj.objective ?? "",
782
+ success_criteria: obj.success_criteria ?? [],
783
+ hard_constraints: obj.hard_constraints ?? [],
784
+ created_at_round: obj.created_at_round ?? 1,
785
+ loop_id: obj.loop_id ?? "",
786
+ });
787
+ }
788
+ // Hydrate vault context for cross-round memory
789
+ let context = hydrateResults ?? null;
790
+ if (context === null && lcr.loop_id && lcr.round > 1) {
791
+ // v3.0.1: the compile round anchors the hydration cache — warm caches
792
+ // skip the read entirely; otherwise only rounds committed since the
793
+ // last hydration are read.
794
+ context = this.hydrateLoopContext(lcr.loop_id, lcr.round);
795
+ }
796
+ // Delegate to pure-function compiler
797
+ let response;
798
+ try {
799
+ response = compileLoop(lcr, context);
800
+ }
801
+ catch (exc) {
802
+ return {
803
+ status: AgentStatus.ERROR,
804
+ response: {
805
+ status: AgentStatus.ERROR,
806
+ prompt: null,
807
+ error: `loop_compile failed: ${exc}`,
808
+ },
809
+ };
810
+ }
811
+ // Persist lineage to vault
812
+ if (options.persistLineage !== false) {
813
+ this.persistLoopLineage(response, lcr);
814
+ }
815
+ return {
816
+ status: AgentStatus.OK,
817
+ response: {
818
+ status: AgentStatus.OK,
819
+ prompt: response.prompt,
820
+ error: null,
821
+ state_file_content: response.state_file_content,
822
+ prompt_artifact: response.prompt_artifact,
823
+ warnings: response.warnings,
824
+ // v2.12: Pass the compiler's derived state through for typed
825
+ // projections (consumed by getProjection without re-persisting).
826
+ rolling_summary: response.rolling_summary,
827
+ sub_goals: response.sub_goals,
828
+ suggested_next_task: response.suggested_next_task,
829
+ },
830
+ };
831
+ }
832
+ }
833
+ // ═══════════════════════════════════════════════════════════════════════════
834
+ // Factory
835
+ // ═══════════════════════════════════════════════════════════════════════════
836
+ export function createEngine(store) {
837
+ return new LoopForgeEngine(store);
838
+ }
839
+ //# sourceMappingURL=engine.js.map