loopforge 3.9.2 → 4.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -112
- package/dist/audit.d.ts +73 -15
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +131 -61
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +152 -40
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +382 -206
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +41 -12
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +0 -2
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +0 -4
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +105 -141
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +249 -372
- package/dist/committed-round.js.map +1 -1
- package/dist/constraint-lifecycle.d.ts +73 -0
- package/dist/constraint-lifecycle.d.ts.map +1 -0
- package/dist/constraint-lifecycle.js +174 -0
- package/dist/constraint-lifecycle.js.map +1 -0
- package/dist/context-boundary.d.ts +83 -0
- package/dist/context-boundary.d.ts.map +1 -0
- package/dist/context-boundary.js +101 -0
- package/dist/context-boundary.js.map +1 -0
- package/dist/contract-items.d.ts +12 -8
- package/dist/contract-items.d.ts.map +1 -1
- package/dist/contract-items.js +31 -26
- package/dist/contract-items.js.map +1 -1
- package/dist/contract-requirement.d.ts +47 -0
- package/dist/contract-requirement.d.ts.map +1 -0
- package/dist/contract-requirement.js +51 -0
- package/dist/contract-requirement.js.map +1 -0
- package/dist/criterion-status.d.ts +35 -0
- package/dist/criterion-status.d.ts.map +1 -0
- package/dist/criterion-status.js +89 -0
- package/dist/criterion-status.js.map +1 -0
- package/dist/enforcement-gate.d.ts +32 -67
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +203 -439
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +9 -103
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +28 -887
- package/dist/engine.js.map +1 -1
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +69 -0
- package/dist/errors.js.map +1 -0
- package/dist/evidence-claims.d.ts +33 -31
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +58 -72
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-posture-view.d.ts +19 -0
- package/dist/evidence-posture-view.d.ts.map +1 -0
- package/dist/evidence-posture-view.js +116 -0
- package/dist/evidence-posture-view.js.map +1 -0
- package/dist/evidence-posture.d.ts +73 -0
- package/dist/evidence-posture.d.ts.map +1 -0
- package/dist/evidence-posture.js +281 -0
- package/dist/evidence-posture.js.map +1 -0
- package/dist/evidence-provider.d.ts +55 -37
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +182 -332
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +167 -27
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +373 -58
- package/dist/explain.js.map +1 -1
- package/dist/field-accounting.d.ts.map +1 -1
- package/dist/field-accounting.js +3 -22
- package/dist/field-accounting.js.map +1 -1
- package/dist/generate-schema.js +13 -8
- package/dist/generate-schema.js.map +1 -1
- package/dist/git-boundary.d.ts +128 -0
- package/dist/git-boundary.d.ts.map +1 -0
- package/dist/git-boundary.js +625 -0
- package/dist/git-boundary.js.map +1 -0
- package/dist/git-facts.d.ts +22 -0
- package/dist/git-facts.d.ts.map +1 -0
- package/dist/git-facts.js +29 -0
- package/dist/git-facts.js.map +1 -0
- package/dist/git-motion.d.ts +37 -0
- package/dist/git-motion.d.ts.map +1 -0
- package/dist/git-motion.js +59 -0
- package/dist/git-motion.js.map +1 -0
- package/dist/history.d.ts +44 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +77 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +16 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +80 -14
- package/dist/init.js.map +1 -1
- package/dist/loop-compiler.d.ts +51 -19
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +423 -507
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts +6 -5
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +153 -35
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +285 -81
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +1629 -574
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +61 -19
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +81 -39
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +963 -271
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +23 -31
- package/dist/mcp/submission-boundary.d.ts.map +1 -1
- package/dist/mcp/submission-boundary.js +242 -82
- package/dist/mcp/submission-boundary.js.map +1 -1
- package/dist/mcp/submission-handle.d.ts +66 -0
- package/dist/mcp/submission-handle.d.ts.map +1 -0
- package/dist/mcp/submission-handle.js +173 -0
- package/dist/mcp/submission-handle.js.map +1 -0
- package/dist/mcp/tools.d.ts +17 -7
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +763 -244
- package/dist/mcp/tools.js.map +1 -1
- package/dist/open-question.d.ts +7 -0
- package/dist/open-question.d.ts.map +1 -0
- package/dist/open-question.js +14 -0
- package/dist/open-question.js.map +1 -0
- package/dist/policy-metrics.d.ts +31 -21
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +117 -64
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +15 -40
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +13 -13
- package/dist/policy.js.map +1 -1
- package/dist/progress.d.ts +45 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +136 -0
- package/dist/progress.js.map +1 -0
- package/dist/prompt-assembler.d.ts +28 -9
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +592 -603
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-coverage.d.ts +39 -0
- package/dist/prompt-coverage.d.ts.map +1 -0
- package/dist/prompt-coverage.js +80 -0
- package/dist/prompt-coverage.js.map +1 -0
- package/dist/prompt-freshness.d.ts +25 -0
- package/dist/prompt-freshness.d.ts.map +1 -0
- package/dist/prompt-freshness.js +88 -0
- package/dist/prompt-freshness.js.map +1 -0
- package/dist/prompt-policy.d.ts +14 -4
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +29 -10
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +875 -419
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +73 -99
- package/dist/protocol.js.map +1 -1
- package/dist/recovery-guidance.d.ts +13 -0
- package/dist/recovery-guidance.d.ts.map +1 -0
- package/dist/recovery-guidance.js +15 -0
- package/dist/recovery-guidance.js.map +1 -0
- package/dist/recovery.d.ts +68 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +234 -0
- package/dist/recovery.js.map +1 -0
- package/dist/replay.d.ts +42 -8
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +311 -84
- package/dist/replay.js.map +1 -1
- package/dist/result-facts.d.ts +45 -0
- package/dist/result-facts.d.ts.map +1 -0
- package/dist/result-facts.js +86 -0
- package/dist/result-facts.js.map +1 -0
- package/dist/round-contract.d.ts +38 -8
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +71 -31
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +45 -91
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +254 -271
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +56 -16
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +225 -39
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +33 -52
- package/dist/round-facts.d.ts.map +1 -1
- package/dist/round-facts.js +54 -157
- package/dist/round-facts.js.map +1 -1
- package/dist/round-guidance.d.ts +56 -0
- package/dist/round-guidance.d.ts.map +1 -0
- package/dist/round-guidance.js +161 -0
- package/dist/round-guidance.js.map +1 -0
- package/dist/round-outcome.d.ts +21 -0
- package/dist/round-outcome.d.ts.map +1 -0
- package/dist/round-outcome.js +31 -0
- package/dist/round-outcome.js.map +1 -0
- package/dist/round-transaction.d.ts +168 -21
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +856 -39
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +40 -60
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +455 -384
- package/dist/self-eval.js.map +1 -1
- package/dist/storage.d.ts +16 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +54 -4
- package/dist/storage.js.map +1 -1
- package/dist/subgoal-state.d.ts +2 -3
- package/dist/subgoal-state.d.ts.map +1 -1
- package/dist/subgoal-state.js +2 -3
- package/dist/subgoal-state.js.map +1 -1
- package/dist/submission-guidance.d.ts +43 -21
- package/dist/submission-guidance.d.ts.map +1 -1
- package/dist/submission-guidance.js +368 -262
- package/dist/submission-guidance.js.map +1 -1
- package/dist/submission-receipt.d.ts +16 -8
- package/dist/submission-receipt.d.ts.map +1 -1
- package/dist/submission-receipt.js +146 -51
- package/dist/submission-receipt.js.map +1 -1
- package/dist/token-utils.d.ts +47 -7
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +66 -15
- package/dist/token-utils.js.map +1 -1
- package/dist/transaction-identity.d.ts +9 -0
- package/dist/transaction-identity.d.ts.map +1 -0
- package/dist/transaction-identity.js +69 -0
- package/dist/transaction-identity.js.map +1 -0
- package/dist/verification-actions.d.ts +13 -0
- package/dist/verification-actions.d.ts.map +1 -0
- package/dist/verification-actions.js +44 -0
- package/dist/verification-actions.js.map +1 -0
- package/dist/verification-gate.d.ts +26 -46
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +218 -427
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +187 -0
- package/dist/working-memory.d.ts.map +1 -0
- package/dist/working-memory.js +767 -0
- package/dist/working-memory.js.map +1 -0
- package/dist/workspace-checkpoint.d.ts +88 -0
- package/dist/workspace-checkpoint.d.ts.map +1 -0
- package/dist/workspace-checkpoint.js +326 -0
- package/dist/workspace-checkpoint.js.map +1 -0
- package/dist/workspace-restore.d.ts +68 -0
- package/dist/workspace-restore.d.ts.map +1 -0
- package/dist/workspace-restore.js +604 -0
- package/dist/workspace-restore.js.map +1 -0
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +0 -17
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +5 -19
- package/package.json +2 -2
- package/skills/forge/SKILL.md +212 -0
- package/skills/forge/references/evidence-and-contracts.md +204 -0
- package/skills/forge/references/recovery-and-views.md +188 -0
- package/dist/cognitive-facts.d.ts +0 -30
- package/dist/cognitive-facts.d.ts.map +0 -1
- package/dist/cognitive-facts.js +0 -113
- package/dist/cognitive-facts.js.map +0 -1
- package/dist/loop-extras-parser.d.ts +0 -81
- package/dist/loop-extras-parser.d.ts.map +0 -1
- package/dist/loop-extras-parser.js +0 -213
- package/dist/loop-extras-parser.js.map +0 -1
- package/dist/loop-projection.d.ts +0 -6
- package/dist/loop-projection.d.ts.map +0 -1
- package/dist/loop-projection.js +0 -28
- package/dist/loop-projection.js.map +0 -1
- package/skills/perception/SKILL.md +0 -370
package/dist/engine.js
CHANGED
|
@@ -1,37 +1,21 @@
|
|
|
1
|
-
/** LoopForge-loop_compile — Engine (
|
|
1
|
+
/** LoopForge-loop_compile — Engine (compile façade).
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* silent-failure counters for observability.
|
|
3
|
+
* The engine loads committed history and delegates to the pure compiler. It
|
|
4
|
+
* no longer writes round history; the round transaction coordinator owns the
|
|
5
|
+
* one durable write path.
|
|
7
6
|
*/
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
9
7
|
import { getPolicy } from "./policy.js";
|
|
10
|
-
import { FileLoopStore
|
|
11
|
-
import { AgentStatus,
|
|
8
|
+
import { FileLoopStore } from "./loop-store.js";
|
|
9
|
+
import { AgentStatus, makeLoopCompileResponse, } from "./protocol.js";
|
|
12
10
|
import { compileLoop } from "./loop-compiler.js";
|
|
13
|
-
import {
|
|
14
|
-
import { isRecord } from "./token-utils.js";
|
|
15
|
-
import { decodeCommittedRound } from "./committed-round.js";
|
|
16
|
-
import { policyMetrics } from "./policy-metrics.js";
|
|
17
|
-
import { parseExecutionReport, parseCriterionRevisions, parseSubGoalUpdates, parseWorkerResults, parsePromptRequests, } from "./self-eval.js";
|
|
18
|
-
import { parseLoopExtras } from "./loop-extras-parser.js";
|
|
11
|
+
import { loadCommittedHistory } from "./history.js";
|
|
19
12
|
// ── Re-export self-evaluation utilities (moved to self-eval.ts) ──────────
|
|
20
|
-
export { parseExecutionReport,
|
|
21
|
-
function makeEngineMetrics() {
|
|
22
|
-
return {
|
|
23
|
-
vaultWriteErrors: 0,
|
|
24
|
-
sessionStart: Date.now(),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
13
|
+
export { parseExecutionReport, parseWorkerResults, buildSelfEvaluation, } from "./self-eval.js";
|
|
27
14
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
28
15
|
// LoopForgeEngine
|
|
29
16
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
30
17
|
export class LoopForgeEngine {
|
|
31
|
-
state = null;
|
|
32
18
|
store = null;
|
|
33
|
-
metrics = null;
|
|
34
|
-
lastTask = null;
|
|
35
19
|
constructor(store) {
|
|
36
20
|
this.store = store ?? null;
|
|
37
21
|
}
|
|
@@ -46,878 +30,35 @@ export class LoopForgeEngine {
|
|
|
46
30
|
getStore() {
|
|
47
31
|
return this.resolveStore();
|
|
48
32
|
}
|
|
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
33
|
// ═══════════════════════════════════════════════════════════════════════
|
|
65
|
-
//
|
|
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
|
-
// v3.3.1: a feedback write that replaces an already-cached round
|
|
79
|
-
// (a backtrack redo reuses the same roundId, per the v3.2.1 replace
|
|
80
|
-
// semantics) leaves the hydration cache holding the stale merged
|
|
81
|
-
// entry — coveredRound has already passed that round, so the
|
|
82
|
-
// incremental path never re-reads it and the compiler would keep
|
|
83
|
-
// seeing the rolled-back decision (forced L2, stale state) for the
|
|
84
|
-
// rest of the process. Invalidate so the next compile rehydrates.
|
|
85
|
-
this.invalidateHydrationCacheForFeedback(String(entry.task_id ?? ""));
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
if (this.metrics)
|
|
90
|
-
this.metrics.vaultWriteErrors++;
|
|
91
|
-
console.warn(`LoopForge: vault write failed (feedback_persist) — ${error}`);
|
|
92
|
-
logEvent("vault_write_error", { error: "feedback_persist" });
|
|
93
|
-
policyMetrics.recordVaultWriteError("feedback_persist");
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
buildFeedbackEntry(signal) {
|
|
98
|
-
try {
|
|
99
|
-
return {
|
|
100
|
-
id: randomUUID(),
|
|
101
|
-
task_id: signal.task_id ?? "feedback",
|
|
102
|
-
version_tag: "v1",
|
|
103
|
-
is_active: true,
|
|
104
|
-
timestamp: new Date().toISOString().replace(/\.\d+Z$/, ""),
|
|
105
|
-
user_intent: String(signal.task_type ?? "").slice(0, 200),
|
|
106
|
-
success: signal.success ?? false,
|
|
107
|
-
execution_feedback: JSON.stringify({
|
|
108
|
-
success: signal.success ?? false,
|
|
109
|
-
status: signal.success ? "success" : "partial",
|
|
110
|
-
constraint_compliance: {
|
|
111
|
-
all_hard_constraints_met: !Array.isArray(signal.violations) || signal.violations.length === 0,
|
|
112
|
-
violations: signal.violations ?? [],
|
|
113
|
-
},
|
|
114
|
-
output_summary: signal.task_type ?? "",
|
|
115
|
-
improvement_notes: signal.manual_fixes ?? "",
|
|
116
|
-
}),
|
|
117
|
-
task_type: signal.task_type ?? "",
|
|
118
|
-
tags: signal.skill_used ? [signal.skill_used] : [],
|
|
119
|
-
skill_used: signal.skill_used ?? "",
|
|
120
|
-
loop_id: signal.loop_id,
|
|
121
|
-
loop_lineage: signal.loop_lineage ?? {},
|
|
122
|
-
// Persist execution_report so committed claims remain available
|
|
123
|
-
// to verification and the canonical state.
|
|
124
|
-
execution_report: signal.execution_report ?? null,
|
|
125
|
-
discovered_constraints: signal.discovered_constraints ?? [],
|
|
126
|
-
emerged_subtasks: signal.emerged_subtasks ?? [],
|
|
127
|
-
// v3.7.1: explicit transitions persist on committed entries so the
|
|
128
|
-
// compiler replays them in round order on every derivation.
|
|
129
|
-
subgoal_updates: signal.subgoal_updates ?? [],
|
|
130
|
-
retracted_constraints: signal.retracted_constraints ?? [],
|
|
131
|
-
worker_results: signal.worker_results ?? [],
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
if (this.metrics)
|
|
136
|
-
this.metrics.vaultWriteErrors++;
|
|
137
|
-
logEvent("vault_write_error", { error: "feedback_entry_build" });
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
142
|
-
// Lineage persistence
|
|
143
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
144
|
-
persistLoopLineage(response, request) {
|
|
145
|
-
if (this.metrics === null)
|
|
146
|
-
this.metrics = makeEngineMetrics();
|
|
147
|
-
const loopObjDict = response.loop_objective
|
|
148
|
-
? response.loop_objective
|
|
149
|
-
: null;
|
|
150
|
-
const structuredLineage = {
|
|
151
|
-
loop_id: response.loop_id,
|
|
152
|
-
round: response.round,
|
|
153
|
-
goal_id: response.goal_id,
|
|
154
|
-
goal_text_hash: response.goal_text_hash,
|
|
155
|
-
recompile_level: response.recompile_level,
|
|
156
|
-
constraints_active: response.constraints_active,
|
|
157
|
-
task: request.task,
|
|
158
|
-
success: true,
|
|
159
|
-
};
|
|
160
|
-
// v3.8.1: the `presented_*` lineage stamps are gone with L1 collapse.
|
|
161
|
-
// They were persisted derived state whose only purpose was to change what
|
|
162
|
-
// the NEXT prompt rendered — a prompt's content must follow from
|
|
163
|
-
// committed facts, not from what the previous prompt happened to show.
|
|
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)
|
|
34
|
+
// Loop Compile (public, primary)
|
|
203
35
|
// ═══════════════════════════════════════════════════════════════════════
|
|
204
|
-
|
|
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
|
-
outcome: e.outcome,
|
|
233
|
-
discoveredConstraints: e.discoveredConstraints,
|
|
234
|
-
})),
|
|
235
|
-
},
|
|
236
|
-
};
|
|
36
|
+
invokeLoopCompile(request, options = {}) {
|
|
237
37
|
try {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
console.warn(`LoopForge: vault write failed (delegation_persist) — ${error}`);
|
|
244
|
-
logEvent("vault_write_error", { error: "record_delegation" });
|
|
245
|
-
policyMetrics.recordVaultWriteError("delegation_persist");
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
249
|
-
// Hydrate loop context from vault
|
|
250
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
251
|
-
/** Eval-owned fields merged from the committed round decision onto the
|
|
252
|
-
* lineage entry. These are the agent's report of what happened during the
|
|
253
|
-
* round; compile-time lineage fields (constraints_active, goal_text_hash,
|
|
254
|
-
* recompile_level, …) are left untouched. */
|
|
255
|
-
static EVAL_MERGED_LINEAGE_FIELDS = [
|
|
256
|
-
"execution_report",
|
|
257
|
-
"compression_checkpoint",
|
|
258
|
-
"checkpoint_label",
|
|
259
|
-
"discovered_constraints",
|
|
260
|
-
"emerged_subtasks",
|
|
261
|
-
"subgoal_updates",
|
|
262
|
-
"retracted_constraints",
|
|
263
|
-
"wrong_assumptions",
|
|
264
|
-
"objective_refinement",
|
|
265
|
-
"outcome",
|
|
266
|
-
"blocker",
|
|
267
|
-
"revised_success_criteria",
|
|
268
|
-
"worker_results",
|
|
269
|
-
"prompt_requests",
|
|
270
|
-
"round_contract",
|
|
271
|
-
];
|
|
272
|
-
/** Merge the committed round decision (feedback entry) into the compile-time
|
|
273
|
-
* lineage entry so the compiler and projections see the full per-round
|
|
274
|
-
* truth: verification flags, round success, and the agent's own evaluation. */
|
|
275
|
-
mergeCommittedRound(entry, lineage, fb) {
|
|
276
|
-
const committed = decodeCommittedRound(fb);
|
|
277
|
-
const result = committed?.result ?? null;
|
|
278
|
-
const evaluation = committed?.evaluation ?? null;
|
|
279
|
-
if (result) {
|
|
280
|
-
if (Array.isArray(result.verificationFlags) &&
|
|
281
|
-
result.verificationFlags.length > 0) {
|
|
282
|
-
entry.verification_flags = result.verificationFlags;
|
|
283
|
-
}
|
|
284
|
-
if (typeof result.roundSuccess === "boolean") {
|
|
285
|
-
entry.success = result.roundSuccess;
|
|
286
|
-
lineage.success = result.roundSuccess;
|
|
287
|
-
}
|
|
288
|
-
// The committed decision's action — lets the compiler detect a
|
|
289
|
-
// post-backtrack recovery boundary (L2 rehydration) without a
|
|
290
|
-
// second persistence format.
|
|
291
|
-
lineage.committed_action = result.action;
|
|
292
|
-
// v3.7.1: Recovery Brief facts stamped onto the merged lineage entry
|
|
293
|
-
// (same derived-stamp pattern as attempt/round_evidence — in-memory
|
|
294
|
-
// only, never persisted; a fresh hydration re-derives them from the
|
|
295
|
-
// committed rollback decision). Compile-time views need them on the
|
|
296
|
-
// merged row because raw feedback documents are never exposed.
|
|
297
|
-
if (result.action === "backtrack") {
|
|
298
|
-
if (typeof result.backtrackTarget === "number") {
|
|
299
|
-
lineage.backtrackTarget = result.backtrackTarget;
|
|
300
|
-
}
|
|
301
|
-
if (typeof result.backtrackTriggerRule === "string") {
|
|
302
|
-
lineage.backtrackTriggerRule = result.backtrackTriggerRule;
|
|
303
|
-
}
|
|
304
|
-
if (Array.isArray(result.backtrackFailedRounds)) {
|
|
305
|
-
lineage.backtrackFailedRounds = result.backtrackFailedRounds;
|
|
306
|
-
}
|
|
307
|
-
if (Array.isArray(result.backtrackApproaches)) {
|
|
308
|
-
lineage.backtrackApproaches = result.backtrackApproaches;
|
|
309
|
-
}
|
|
310
|
-
if (Array.isArray(result.backtrackWrongAssumptions)) {
|
|
311
|
-
lineage.backtrackWrongAssumptions = result.backtrackWrongAssumptions;
|
|
312
|
-
}
|
|
313
|
-
// v3.8.1: the restore facts the agent-facing brief is made of. They
|
|
314
|
-
// were persisted on the committed decision but never stamped here, so
|
|
315
|
-
// the compile-side Recovery Brief could not read them off the merged
|
|
316
|
-
// row and re-derived the rollback from a second walk instead.
|
|
317
|
-
if (Array.isArray(result.backtrackSkippedFiles)) {
|
|
318
|
-
lineage.backtrackSkippedFiles = result.backtrackSkippedFiles;
|
|
319
|
-
}
|
|
320
|
-
if (result.backtrackSkippedFingerprints &&
|
|
321
|
-
typeof result.backtrackSkippedFingerprints === "object") {
|
|
322
|
-
lineage.backtrackSkippedFingerprints = result.backtrackSkippedFingerprints;
|
|
323
|
-
}
|
|
324
|
-
if (typeof result.backtrackTargetGitHead === "string") {
|
|
325
|
-
lineage.backtrackTargetGitHead = result.backtrackTargetGitHead;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
if (committed) {
|
|
330
|
-
// v3.9.1: the merged entry carries the SAME transaction snapshot the
|
|
331
|
-
// feedback entry persisted. decodeMergedRound reads that one envelope
|
|
332
|
-
// (never a second raw-field reconstruction), so the compile path and
|
|
333
|
-
// the durable read model see one evaluation/history interpretation.
|
|
334
|
-
// In-memory only — disk lineage entries are re-hydrated from feedback.
|
|
335
|
-
const transaction = isRecord(fb.loop_lineage)
|
|
336
|
-
? fb.loop_lineage.round_transaction
|
|
337
|
-
: undefined;
|
|
338
|
-
if (isRecord(transaction)) {
|
|
339
|
-
lineage.round_transaction = transaction;
|
|
340
|
-
}
|
|
341
|
-
// v3.5.1: compile-side display readers (Round Stats rejected attempts,
|
|
342
|
-
// the Machine (git) dashboard row) never see raw :feedback entries —
|
|
343
|
-
// only this merged view — so the machine-observed data they need is
|
|
344
|
-
// stamped here. In-memory only (hydration cache; disk lineage entries
|
|
345
|
-
// never carry the stamp — a fresh hydration re-derives it identically
|
|
346
|
-
// from the feedback entry's transaction snapshot).
|
|
347
|
-
lineage.attempt = committed.attempt;
|
|
348
|
-
// v3.8.1: the ContractBinding is stamped with the same reasoning, and
|
|
349
|
-
// for a sharper reason: `decodeMergedRound` reads it from
|
|
350
|
-
// `lineage.contract_binding` and the compile path sees ONLY merged
|
|
351
|
-
// entries, so an unstamped binding made the compile-side "same
|
|
352
|
-
// configuration as at declaration" check silently skip — the prompt and
|
|
353
|
-
// state file called a contract closed while the coordinator, explain and
|
|
354
|
-
// audit (which decode the durable :feedback entry) reported it open and
|
|
355
|
-
// insufficient. One committed fact, two answers.
|
|
356
|
-
if (committed.contractBinding) {
|
|
357
|
-
lineage.contract_binding = committed.contractBinding;
|
|
358
|
-
}
|
|
359
|
-
// v3.8: the factual observation collections are stamped (the round
|
|
360
|
-
// delta is derived from them by the read model — transaction schema 3
|
|
361
|
-
// persists no pre-computed delta).
|
|
362
|
-
if (committed.beforeEvidence.length > 0) {
|
|
363
|
-
lineage.before_evidence = committed.beforeEvidence;
|
|
364
|
-
}
|
|
365
|
-
if (committed.afterEvidence.length > 0) {
|
|
366
|
-
lineage.after_evidence = committed.afterEvidence;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (!evaluation)
|
|
370
|
-
return;
|
|
371
|
-
for (const key of LoopForgeEngine.EVAL_MERGED_LINEAGE_FIELDS) {
|
|
372
|
-
const value = evaluation[key];
|
|
373
|
-
if (value === undefined)
|
|
374
|
-
continue;
|
|
375
|
-
lineage[key] = value;
|
|
376
|
-
entry[key] = value;
|
|
377
|
-
}
|
|
378
|
-
if (typeof evaluation.success === "boolean") {
|
|
379
|
-
entry.success = evaluation.success;
|
|
380
|
-
lineage.success = evaluation.success;
|
|
381
|
-
}
|
|
382
|
-
if (typeof evaluation.output_summary === "string" && evaluation.output_summary) {
|
|
383
|
-
entry.output_summary = evaluation.output_summary;
|
|
384
|
-
}
|
|
385
|
-
if (Array.isArray(evaluation.constraint_violations)) {
|
|
386
|
-
entry.constraint_violations = evaluation.constraint_violations;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
/** v3.0.1: Per-engine hydration cache. The vault is append-only per round —
|
|
390
|
-
* between two compiles of one loop only the newest round(s) changed. The
|
|
391
|
-
* cache holds the merged lineage entries (the exact shape the compiler
|
|
392
|
-
* consumes; feedback bodies are merged in, never returned), so a long-lived
|
|
393
|
-
* process compiles incrementally: one round document read per round
|
|
394
|
-
* boundary instead of the full history. Nothing is persisted; a fresh
|
|
395
|
-
* engine hydrates fully once. `coveredRound` is the highest CONTIGUOUS
|
|
396
|
-
* COMMITTED round (feedback merged) whose entries are in the cache — the
|
|
397
|
-
* compiler only reads rounds below the compile round, so
|
|
398
|
-
* `coveredRound >= round - 1` means the cache is complete for that compile.
|
|
399
|
-
* Lineage-only entries of not-yet-committed rounds may trail the cache but
|
|
400
|
-
* never advance coveredRound (a later read replaces them post-commit). */
|
|
401
|
-
hydrationCache = null;
|
|
402
|
-
/** v3.7.1: Drop the hydration cache after a committed BACKTRACK decision.
|
|
403
|
-
* The rollback commits as the CURRENT round (cache.coveredRound + 1), so
|
|
404
|
-
* the normal incremental path never re-reads it before the restore
|
|
405
|
-
* compile — which targets the SAME round and must see the rollback
|
|
406
|
-
* immediately (recovery-boundary L2, Recovery Brief facts). One full
|
|
407
|
-
* rehydrate on the rare rollback path is the correct trade. */
|
|
408
|
-
invalidateHydrationCache(loopId) {
|
|
409
|
-
if (this.hydrationCache?.loopId === loopId)
|
|
410
|
-
this.hydrationCache = null;
|
|
411
|
-
}
|
|
412
|
-
/** v3.3.1: Drop the hydration cache when a feedback write targets a round
|
|
413
|
-
* the cache has already merged (coveredRound >= round). Cache entries are
|
|
414
|
-
* LINEAGE task_ids (loop:…:rN), so a task_id collision test could never
|
|
415
|
-
* fire for feedback writes (loop:…:rN:feedback) — the round number is the
|
|
416
|
-
* real collision signal. A backtrack redo reuses the roundId and REPLACES
|
|
417
|
-
* the round's committed feedback (v3.2.1 semantics); without this the
|
|
418
|
-
* fast path (coveredRound >= compileRound - 1) would serve the stale
|
|
419
|
-
* rolled-back entry for the rest of the process. A genuinely new round
|
|
420
|
-
* (round = coveredRound + 1) lands past the cache point and is merged by
|
|
421
|
-
* the next incremental read — no invalidation. Lineage writes never
|
|
422
|
-
* invalidate: they precede the commit by design. */
|
|
423
|
-
invalidateHydrationCacheForFeedback(taskId) {
|
|
424
|
-
const cache = this.hydrationCache;
|
|
425
|
-
if (!cache)
|
|
426
|
-
return;
|
|
427
|
-
const round = LoopForgeEngine.feedbackRound(taskId);
|
|
428
|
-
if (round !== null && cache.coveredRound >= round) {
|
|
429
|
-
this.hydrationCache = null;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
/** The round a feedback entry belongs to, or null. */
|
|
433
|
-
static feedbackRound(taskId) {
|
|
434
|
-
const parts = taskId.split(":r");
|
|
435
|
-
if (parts.length < 2)
|
|
436
|
-
return null;
|
|
437
|
-
const roundStr = parts[1].split(":")[0];
|
|
438
|
-
const round = parseInt(roundStr, 10);
|
|
439
|
-
return Number.isNaN(round) ? null : round;
|
|
440
|
-
}
|
|
441
|
-
/** Highest contiguous round (from 1) present in the given feedback entries. */
|
|
442
|
-
static contiguousCommittedRound(fbEntries) {
|
|
443
|
-
const committed = new Set();
|
|
444
|
-
for (const fe of fbEntries) {
|
|
445
|
-
const round = LoopForgeEngine.feedbackRound(String(fe.task_id ?? ""));
|
|
446
|
-
if (round !== null)
|
|
447
|
-
committed.add(round);
|
|
448
|
-
}
|
|
449
|
-
let covered = 0;
|
|
450
|
-
while (committed.has(covered + 1))
|
|
451
|
-
covered++;
|
|
452
|
-
return covered;
|
|
453
|
-
}
|
|
454
|
-
/** Merge committed feedback entries into lineage entries and apply the
|
|
455
|
-
* canonical output_summary / constraint_violations projection. Shared by the
|
|
456
|
-
* full and incremental hydration paths so both produce identical shapes.
|
|
457
|
-
*
|
|
458
|
-
* The round commit entry (`loop:<id>:r<N>:feedback`) carries the full
|
|
459
|
-
* committed decision — verification flags, round success, and the agent's
|
|
460
|
-
* SelfEvaluation — which the compiler needs to derive milestones,
|
|
461
|
-
* constraint decay, sub-goal accumulation, and rolling summaries. Without
|
|
462
|
-
* this merge the cross-round features only see the 8 compile-time lineage
|
|
463
|
-
* fields. */
|
|
464
|
-
buildMergedEntries(fresh, fbFresh) {
|
|
465
|
-
const fbByRound = new Map();
|
|
466
|
-
for (const fe of fbFresh) {
|
|
467
|
-
const fbRound = LoopForgeEngine.feedbackRound(String(fe.task_id ?? ""));
|
|
468
|
-
if (fbRound !== null)
|
|
469
|
-
fbByRound.set(fbRound, fe);
|
|
470
|
-
}
|
|
471
|
-
for (const entry of fresh) {
|
|
472
|
-
// v3.7: the legacy top-level `lineage` alias was removed.
|
|
473
|
-
// Only compile-lineage rows absorb the committed round's feedback.
|
|
474
|
-
// Delegation journals (`loop:<id>:r<N>:delegations`) share the same
|
|
475
|
-
// lineage.round and would otherwise decode as a SECOND round view —
|
|
476
|
-
// double-counting violations/lessons for one committed round.
|
|
477
|
-
if (entry.task_type !== "loop_lineage")
|
|
478
|
-
continue;
|
|
479
|
-
const lineage = (entry.loop_lineage ?? {});
|
|
480
|
-
const rnd = lineage.round;
|
|
481
|
-
const fb = rnd ? fbByRound.get(rnd) : undefined;
|
|
482
|
-
if (fb)
|
|
483
|
-
this.mergeCommittedRound(entry, lineage, fb);
|
|
484
|
-
if (!entry.output_summary) {
|
|
485
|
-
entry.output_summary = lineage.output_summary ?? "";
|
|
486
|
-
}
|
|
487
|
-
if (!entry.constraint_violations) {
|
|
488
|
-
entry.constraint_violations =
|
|
489
|
-
lineage.constraint_violations ?? [];
|
|
38
|
+
let context = null;
|
|
39
|
+
let history;
|
|
40
|
+
if (request.loop_id) {
|
|
41
|
+
history = loadCommittedHistory(this.resolveStore(), request.loop_id);
|
|
42
|
+
context = { results: history.entries };
|
|
490
43
|
}
|
|
44
|
+
return compileLoop(request, context, options, history);
|
|
491
45
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const cache = this.hydrationCache;
|
|
502
|
-
if (cache.coveredRound >= round - 1) {
|
|
503
|
-
return { results: cache.entries, global_entries: [] };
|
|
504
|
-
}
|
|
505
|
-
const sinceRound = cache.coveredRound + 1;
|
|
506
|
-
const fresh = queryLoopEntries(store, loopId, { prefix, sinceRound });
|
|
507
|
-
const freshFb = queryLoopEntries(store, loopId, {
|
|
508
|
-
prefix,
|
|
509
|
-
sinceRound,
|
|
510
|
-
feedbackOnly: true,
|
|
511
|
-
});
|
|
512
|
-
const merged = this.buildMergedEntries(fresh, freshFb);
|
|
513
|
-
for (const entry of merged) {
|
|
514
|
-
const tid = String(entry.task_id ?? "");
|
|
515
|
-
// Replace-by-task_id: a round read earlier as a lineage-only entry
|
|
516
|
-
// (not yet committed) must be replaced by its post-commit merged
|
|
517
|
-
// version — a stale unmerged entry would silently drop the round's
|
|
518
|
-
// committed decision (verification flags, committed_action, eval).
|
|
519
|
-
const index = cache.entries.findIndex((e) => String(e.task_id ?? "") === tid);
|
|
520
|
-
if (index >= 0)
|
|
521
|
-
cache.entries[index] = entry;
|
|
522
|
-
else
|
|
523
|
-
cache.entries.push(entry);
|
|
524
|
-
}
|
|
525
|
-
// Advance coveredRound only for rounds that actually committed since
|
|
526
|
-
// the cache point — the fresh reads start at coveredRound + 1, so the
|
|
527
|
-
// contiguous run extends from there.
|
|
528
|
-
const freshCommitted = new Set();
|
|
529
|
-
for (const fe of freshFb) {
|
|
530
|
-
const r = LoopForgeEngine.feedbackRound(String(fe.task_id ?? ""));
|
|
531
|
-
if (r !== null)
|
|
532
|
-
freshCommitted.add(r);
|
|
46
|
+
catch (exc) {
|
|
47
|
+
// Projection-coverage and operator-acceptance integrity defects are
|
|
48
|
+
// FAIL CLOSED signals, not compile failures. Rethrow so the MCP
|
|
49
|
+
// boundary surfaces their stable codes.
|
|
50
|
+
const code = exc?.code;
|
|
51
|
+
if (code === "projection_incomplete" ||
|
|
52
|
+
code === "operator_acceptance_conflict" ||
|
|
53
|
+
code === "legacy_history") {
|
|
54
|
+
throw exc;
|
|
533
55
|
}
|
|
534
|
-
|
|
535
|
-
cache.coveredRound++;
|
|
536
|
-
return { results: cache.entries, global_entries: [] };
|
|
537
|
-
}
|
|
538
|
-
// Full path — cold engine, different loop, or a round-less hydrate (e.g.
|
|
539
|
-
// health checks). Merge all feedback entries, dedupe by task_id (round
|
|
540
|
-
// documents hold the latest write per key), and cache the merged view.
|
|
541
|
-
// coveredRound counts only contiguous COMMITTED rounds — lineage-only
|
|
542
|
-
// entries of uncommitted rounds may trail the cache but must not make the
|
|
543
|
-
// fast path serve unmerged state.
|
|
544
|
-
const results = queryLoopEntries(store, loopId, { prefix });
|
|
545
|
-
const fbEntries = queryLoopEntries(store, loopId, { prefix, feedbackOnly: true });
|
|
546
|
-
const merged = this.buildMergedEntries(results, fbEntries);
|
|
547
|
-
if (!merged.length)
|
|
548
|
-
return null;
|
|
549
|
-
const byTaskId = new Map();
|
|
550
|
-
for (const entry of merged)
|
|
551
|
-
byTaskId.set(String(entry.task_id ?? ""), entry);
|
|
552
|
-
const entries = [...byTaskId.values()];
|
|
553
|
-
this.hydrationCache = {
|
|
554
|
-
loopId,
|
|
555
|
-
coveredRound: LoopForgeEngine.contiguousCommittedRound(fbEntries),
|
|
556
|
-
entries,
|
|
557
|
-
};
|
|
558
|
-
return { results: entries, global_entries: [] };
|
|
559
|
-
}
|
|
560
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
561
|
-
// Feedback (public)
|
|
562
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
563
|
-
invokeFeedback(request, _hydrateResults) {
|
|
564
|
-
this.ensureInit(request);
|
|
565
|
-
this.lastTask = request.task;
|
|
566
|
-
const fb = request.feedback;
|
|
567
|
-
if (!fb) {
|
|
568
|
-
return {
|
|
569
|
-
status: AgentStatus.ERROR,
|
|
570
|
-
response: {
|
|
571
|
-
status: AgentStatus.ERROR,
|
|
572
|
-
prompt: null,
|
|
573
|
-
error: "Feedback mode requires a feedback payload.",
|
|
574
|
-
},
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
const success = fb.success;
|
|
578
|
-
const violations = fb.constraint_violations ?? [];
|
|
579
|
-
const fixes = fb.manual_fixes_needed ?? "";
|
|
580
|
-
// Loop-aware task_id for feedback→lineage backfill
|
|
581
|
-
const loopId = request.loop_id;
|
|
582
|
-
const loopRound = request.round;
|
|
583
|
-
// v2.14: feedback entries are loop-scoped — a request without loop_id
|
|
584
|
-
// and round can never be persisted. Fail loudly instead of reporting
|
|
585
|
-
// "Feedback Recorded" for a write that was silently dropped.
|
|
586
|
-
if (!loopId || loopRound === undefined) {
|
|
587
|
-
return {
|
|
56
|
+
return makeLoopCompileResponse({
|
|
588
57
|
status: AgentStatus.ERROR,
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
prompt: null,
|
|
592
|
-
error: "Feedback requires loop_id and round — feedback entries are loop-scoped.",
|
|
593
|
-
},
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
const taskId = `loop:${loopId}:r${loopRound}:feedback`;
|
|
597
|
-
const signal = {
|
|
598
|
-
task_id: taskId,
|
|
599
|
-
task_type: request.task.slice(0, 80),
|
|
600
|
-
success,
|
|
601
|
-
skill_used: request.skill_name ?? "",
|
|
602
|
-
violations,
|
|
603
|
-
manual_fixes: fixes,
|
|
604
|
-
loop_id: loopId,
|
|
605
|
-
round: loopRound,
|
|
606
|
-
};
|
|
607
|
-
const persisted = this.persistFeedbackToVault(signal);
|
|
608
|
-
if (!persisted) {
|
|
609
|
-
return {
|
|
610
|
-
status: AgentStatus.ERROR,
|
|
611
|
-
response: {
|
|
612
|
-
status: AgentStatus.ERROR,
|
|
613
|
-
prompt: null,
|
|
614
|
-
error: "Feedback could not be persisted to the loop store — the round was not recorded.",
|
|
615
|
-
},
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
// Update state
|
|
619
|
-
this.state.call_count++;
|
|
620
|
-
this.state.success_trend.push(success);
|
|
621
|
-
if (this.state.success_trend.length > 20) {
|
|
622
|
-
this.state.success_trend = this.state.success_trend.slice(-20);
|
|
623
|
-
}
|
|
624
|
-
logEvent("round_complete", {
|
|
625
|
-
success,
|
|
626
|
-
loopId: loopId ?? "unknown",
|
|
627
|
-
round: loopRound ?? this.state.call_count,
|
|
628
|
-
});
|
|
629
|
-
return {
|
|
630
|
-
status: AgentStatus.OK,
|
|
631
|
-
response: {
|
|
632
|
-
status: AgentStatus.OK,
|
|
633
|
-
prompt: `## Feedback Recorded\n\nSuccess: ${success}\nSignals: 1`,
|
|
634
|
-
error: null,
|
|
635
|
-
},
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
639
|
-
// Auto-Feedback (v1.1 — autonomous loop, no human in the loop)
|
|
640
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
641
|
-
/** Record self-evaluation from agent output without human intervention.
|
|
642
|
-
* Converts SelfEvaluation → ExecutionFeedback → vault persistence.
|
|
643
|
-
* P0–P2: Also persists discovered_constraints, objective_refinement,
|
|
644
|
-
* and emerged_subtasks for the compiler to consume next round.
|
|
645
|
-
* Call this BEFORE invokeLoopCompile for the next round so that
|
|
646
|
-
* hydrateLoopContext picks up the latest success flags. */
|
|
647
|
-
autoFeedback(selfEval, loopId, round, task, roundTransaction) {
|
|
648
|
-
this.ensureInit({ task, mode: Mode.FEEDBACK, feedback: null, skill_name: null, task_id: null });
|
|
649
|
-
const fb = makeExecutionFeedback({
|
|
650
|
-
output: selfEval.output_summary,
|
|
651
|
-
success: selfEval.success,
|
|
652
|
-
constraint_violations: selfEval.constraint_violations,
|
|
653
|
-
manual_fixes_needed: "",
|
|
654
|
-
});
|
|
655
|
-
const taskId = `loop:${loopId}:r${round}:feedback`;
|
|
656
|
-
const transactionId = typeof roundTransaction?.round_id === "string"
|
|
657
|
-
? roundTransaction.round_id
|
|
658
|
-
: null;
|
|
659
|
-
const transactionPersisted = (filter) => {
|
|
660
|
-
if (!transactionId)
|
|
661
|
-
return false;
|
|
662
|
-
return queryLoopEntries(this.resolveStore(), loopId, {
|
|
663
|
-
prefix: taskId,
|
|
664
|
-
feedbackOnly: true,
|
|
665
|
-
}).some((entry) => {
|
|
666
|
-
if (entry.task_id !== taskId)
|
|
667
|
-
return false;
|
|
668
|
-
if (filter && !filter(entry))
|
|
669
|
-
return false;
|
|
670
|
-
const lineage = entry.loop_lineage;
|
|
671
|
-
const transaction = lineage?.round_transaction;
|
|
672
|
-
return transaction !== null &&
|
|
673
|
-
typeof transaction === "object" &&
|
|
674
|
-
!Array.isArray(transaction) &&
|
|
675
|
-
transaction.round_id === transactionId;
|
|
676
|
-
});
|
|
677
|
-
};
|
|
678
|
-
// v3.2.1: a committed backtrack decision is a roll-back directive, not
|
|
679
|
-
// the round's outcome. The redo submission reuses the same roundId and
|
|
680
|
-
// must REPLACE the backtrack entry in the vault — otherwise the stale
|
|
681
|
-
// entry keeps poisoning the R4/R5 progress window and the redo's work
|
|
682
|
-
// never lands (it is replayed or its data is ignored forever).
|
|
683
|
-
const alreadyCommitted = transactionPersisted((entry) => {
|
|
684
|
-
const transaction = isRecord(entry.loop_lineage)
|
|
685
|
-
? entry.loop_lineage.round_transaction
|
|
686
|
-
: null;
|
|
687
|
-
const result = isRecord(transaction) ? transaction.result : null;
|
|
688
|
-
return !(isRecord(result) && result.action === "backtrack");
|
|
689
|
-
});
|
|
690
|
-
// v3.9.1: worker_results is a REPORT. Its discoveredConstraints never
|
|
691
|
-
// enters the global constraint stream by itself; the main agent promotes
|
|
692
|
-
// a worker finding explicitly through top-level discovered_constraints.
|
|
693
|
-
const signal = {
|
|
694
|
-
task_id: taskId,
|
|
695
|
-
task_type: task.slice(0, 80),
|
|
696
|
-
success: fb.success,
|
|
697
|
-
skill_used: "",
|
|
698
|
-
violations: selfEval.constraint_violations,
|
|
699
|
-
manual_fixes: "",
|
|
700
|
-
loop_id: loopId,
|
|
701
|
-
round,
|
|
702
|
-
// P0–P2: Evolution fields
|
|
703
|
-
discovered_constraints: selfEval.discovered_constraints ?? [],
|
|
704
|
-
objective_refinement: selfEval.objective_refinement ?? "",
|
|
705
|
-
emerged_subtasks: selfEval.emerged_subtasks ?? [],
|
|
706
|
-
// v3.7.1: explicit transitions persist with the committed evaluation;
|
|
707
|
-
// the compiler replays them in round order on every derivation.
|
|
708
|
-
subgoal_updates: selfEval.subgoal_updates ?? [],
|
|
709
|
-
// P4: Execution evidence
|
|
710
|
-
execution_report: selfEval.execution_report ?? null,
|
|
711
|
-
// P5: Self-correction
|
|
712
|
-
retracted_constraints: selfEval.retracted_constraints ?? [],
|
|
713
|
-
revised_success_criteria: selfEval.revised_success_criteria ?? [],
|
|
714
|
-
wrong_assumptions: selfEval.wrong_assumptions ?? [],
|
|
715
|
-
// Multi-agent: Worker delegation results
|
|
716
|
-
worker_results: selfEval.worker_results ?? [],
|
|
717
|
-
// v2.12: Tri-state outcome (audit data source)
|
|
718
|
-
outcome: selfEval.outcome,
|
|
719
|
-
loop_lineage: roundTransaction
|
|
720
|
-
? {
|
|
721
|
-
round,
|
|
722
|
-
round_id: roundTransaction.round_id,
|
|
723
|
-
round_transaction: roundTransaction,
|
|
724
|
-
}
|
|
725
|
-
: {},
|
|
726
|
-
};
|
|
727
|
-
if (!alreadyCommitted) {
|
|
728
|
-
this.persistFeedbackToVault(signal);
|
|
729
|
-
if (transactionId && !transactionPersisted()) {
|
|
730
|
-
throw new Error(`Round feedback commit failed: ${transactionId}`);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
// Multi-agent: Auto-record delegation journal from worker_results
|
|
734
|
-
if (selfEval.worker_results && selfEval.worker_results.length > 0) {
|
|
735
|
-
const entries = selfEval.worker_results.map((w, i) => ({
|
|
736
|
-
index: i + 1,
|
|
737
|
-
agentId: w.agentId,
|
|
738
|
-
subAgentType: w.subAgentType ?? "general-purpose",
|
|
739
|
-
subTask: w.subTask,
|
|
740
|
-
resultSummary: w.resultSummary,
|
|
741
|
-
outcome: w.outcome,
|
|
742
|
-
discoveredConstraints: w.discoveredConstraints ?? [],
|
|
743
|
-
}));
|
|
744
|
-
this.recordDelegation(loopId, round, entries);
|
|
745
|
-
}
|
|
746
|
-
// Replaying a committed transaction may repair derived delegation data,
|
|
747
|
-
// but must never apply the feedback to mutable engine state twice.
|
|
748
|
-
if (alreadyCommitted)
|
|
749
|
-
return fb.success;
|
|
750
|
-
// Update state
|
|
751
|
-
this.state.call_count++;
|
|
752
|
-
this.state.success_trend.push(fb.success);
|
|
753
|
-
if (this.state.success_trend.length > 20) {
|
|
754
|
-
this.state.success_trend = this.state.success_trend.slice(-20);
|
|
755
|
-
}
|
|
756
|
-
// v2.5: Old binary-success circuit breaker removed. The enforcement
|
|
757
|
-
// gate now uses machine-observed Git motion for stall decisions.
|
|
758
|
-
logEvent("round_complete", {
|
|
759
|
-
loopId,
|
|
760
|
-
round,
|
|
761
|
-
success: fb.success,
|
|
762
|
-
});
|
|
763
|
-
return fb.success;
|
|
764
|
-
}
|
|
765
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
766
|
-
// Loop Compile (public, primary)
|
|
767
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
768
|
-
invokeLoopCompile(request, hydrateResults, options = {}) {
|
|
769
|
-
this.ensureInit(request);
|
|
770
|
-
this.lastTask = request.task;
|
|
771
|
-
// Parse extras via typed extraction pipeline (loop-extras-parser.ts).
|
|
772
|
-
// Errors are collected but never thrown — the compiler always gets
|
|
773
|
-
// best-effort defaults so a malformed request doesn't crash the engine.
|
|
774
|
-
const extras = request;
|
|
775
|
-
const { parsed, ctx } = parseLoopExtras(extras, request.task_id ?? "");
|
|
776
|
-
if (ctx.errors.length > 0) {
|
|
777
|
-
logEvent("extras_parse_errors", {
|
|
778
|
-
errors: ctx.errors.map((e) => `${e.field}: ${e.message}`),
|
|
58
|
+
prompt: "",
|
|
59
|
+
error: `loop_compile failed: ${exc}`,
|
|
779
60
|
});
|
|
780
61
|
}
|
|
781
|
-
const lcr = makeLoopCompileRequest({
|
|
782
|
-
loop_id: parsed.loop_id,
|
|
783
|
-
round: parsed.round,
|
|
784
|
-
goal_id: parsed.goal_id,
|
|
785
|
-
task: request.task,
|
|
786
|
-
domain: parsed.domain,
|
|
787
|
-
next_task_proposal: parsed.next_task_proposal,
|
|
788
|
-
plan_source: parsed.plan_source,
|
|
789
|
-
constraints_from_plan: parsed.constraints_from_plan,
|
|
790
|
-
new_since_last_round: parsed.new_since_last_round,
|
|
791
|
-
force_level: parsed.force_level,
|
|
792
|
-
external_context: parsed.external_context,
|
|
793
|
-
max_rounds: parsed.max_rounds,
|
|
794
|
-
verification_flags: parsed.verification_flags,
|
|
795
|
-
attempt: parsed.attempt,
|
|
796
|
-
consecutive_rejections: parsed.consecutive_rejections,
|
|
797
|
-
rejection_notice: parsed.rejection_notice,
|
|
798
|
-
});
|
|
799
|
-
// Convert last_round_result if present (object already validated by parser)
|
|
800
|
-
if (parsed.last_round_result) {
|
|
801
|
-
const rr = parsed.last_round_result;
|
|
802
|
-
// Parse P4 execution evidence (shared helper)
|
|
803
|
-
const executionReport = parseExecutionReport(rr.execution_report);
|
|
804
|
-
// Parse P5 revised_success_criteria (shared parser)
|
|
805
|
-
const revisedCriteria = parseCriterionRevisions(rr.revised_success_criteria);
|
|
806
|
-
lcr.last_round_result = makeLoopRoundResult({
|
|
807
|
-
round: rr.round ?? 0,
|
|
808
|
-
success: rr.success ?? false,
|
|
809
|
-
output_summary: rr.output_summary ?? "",
|
|
810
|
-
constraint_violations: rr.constraint_violations ?? [],
|
|
811
|
-
manual_fixes_needed: rr.manual_fixes_needed ?? "",
|
|
812
|
-
// P0–P2: Cognitive evolution fields
|
|
813
|
-
discovered_constraints: Array.isArray(rr.discovered_constraints)
|
|
814
|
-
? rr.discovered_constraints.filter((v) => typeof v === "string")
|
|
815
|
-
: [],
|
|
816
|
-
objective_refinement: typeof rr.objective_refinement === "string"
|
|
817
|
-
? rr.objective_refinement
|
|
818
|
-
: "",
|
|
819
|
-
emerged_subtasks: Array.isArray(rr.emerged_subtasks)
|
|
820
|
-
? rr.emerged_subtasks.filter((v) => typeof v === "string")
|
|
821
|
-
: [],
|
|
822
|
-
// P4: Execution evidence
|
|
823
|
-
execution_report: executionReport,
|
|
824
|
-
// P5: Self-correction
|
|
825
|
-
retracted_constraints: Array.isArray(rr.retracted_constraints)
|
|
826
|
-
? rr.retracted_constraints.filter((v) => typeof v === "string")
|
|
827
|
-
: [],
|
|
828
|
-
revised_success_criteria: revisedCriteria,
|
|
829
|
-
wrong_assumptions: Array.isArray(rr.wrong_assumptions)
|
|
830
|
-
? rr.wrong_assumptions.filter((v) => typeof v === "string")
|
|
831
|
-
: [],
|
|
832
|
-
// v1.10: Checkpoint boundary
|
|
833
|
-
compression_checkpoint: typeof rr.compression_checkpoint === "boolean" ? rr.compression_checkpoint : false,
|
|
834
|
-
checkpoint_label: typeof rr.checkpoint_label === "string" ? rr.checkpoint_label : "",
|
|
835
|
-
// v3.7.1: Sub-goal lifecycle — explicit transitions (shared parser)
|
|
836
|
-
subgoal_updates: parseSubGoalUpdates(rr.subgoal_updates),
|
|
837
|
-
// Multi-agent: Worker delegation results (shared parser)
|
|
838
|
-
worker_results: parseWorkerResults(rr.worker_results),
|
|
839
|
-
// v2.12: Tri-state outcome + blocker + retroactive claims
|
|
840
|
-
outcome: rr.outcome === "success" || rr.outcome === "partial" ||
|
|
841
|
-
rr.outcome === "failed" || rr.outcome === "blocked"
|
|
842
|
-
? rr.outcome
|
|
843
|
-
: undefined,
|
|
844
|
-
blocker: typeof rr.blocker === "string" && rr.blocker.trim().length > 0
|
|
845
|
-
? rr.blocker.slice(0, 500)
|
|
846
|
-
: undefined,
|
|
847
|
-
retroactiveClaims: Array.isArray(rr.retroactiveClaims)
|
|
848
|
-
? rr.retroactiveClaims
|
|
849
|
-
.filter((item) => isRecord(item) &&
|
|
850
|
-
typeof item.round === "number" && item.round >= 1 &&
|
|
851
|
-
typeof item.claim === "string" && item.claim.length > 0)
|
|
852
|
-
.map((item) => ({ round: item.round, claim: item.claim.slice(0, 500) }))
|
|
853
|
-
.slice(0, 20)
|
|
854
|
-
: [],
|
|
855
|
-
// v3.3.1: prompt_requests — this whitelist rebuild previously
|
|
856
|
-
// dropped it, and since EVERY compile path (MCP advance/retry/
|
|
857
|
-
// backtrack + Runtime) funnels through invokeLoopCompile, the
|
|
858
|
-
// compiler never saw it on the production path: prompt_requests
|
|
859
|
-
// (emphasize/confusion_points) were never consumed. Unit tests fed
|
|
860
|
-
// compileLoop directly, so the gap stayed invisible. Shared lenient
|
|
861
|
-
// parsers mirror the other fields. (v3.8 removed next_action from
|
|
862
|
-
// this list along with the field itself.)
|
|
863
|
-
prompt_requests: parsePromptRequests(rr.prompt_requests),
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
// Convert loop_objective if present (object already validated by parser)
|
|
867
|
-
if (parsed.loop_objective) {
|
|
868
|
-
const obj = parsed.loop_objective;
|
|
869
|
-
lcr.loop_objective = makeLoopObjective({
|
|
870
|
-
objective: obj.objective ?? "",
|
|
871
|
-
success_criteria: obj.success_criteria ?? [],
|
|
872
|
-
hard_constraints: obj.hard_constraints ?? [],
|
|
873
|
-
created_at_round: obj.created_at_round ?? 1,
|
|
874
|
-
loop_id: obj.loop_id ?? "",
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
// Hydrate vault context for cross-round memory
|
|
878
|
-
let context = hydrateResults ?? null;
|
|
879
|
-
if (context === null && lcr.loop_id && lcr.round > 1) {
|
|
880
|
-
// v3.0.1: the compile round anchors the hydration cache — warm caches
|
|
881
|
-
// skip the read entirely; otherwise only rounds committed since the
|
|
882
|
-
// last hydration are read.
|
|
883
|
-
context = this.hydrateLoopContext(lcr.loop_id, lcr.round);
|
|
884
|
-
}
|
|
885
|
-
// Delegate to pure-function compiler
|
|
886
|
-
let response;
|
|
887
|
-
try {
|
|
888
|
-
response = compileLoop(lcr, context);
|
|
889
|
-
}
|
|
890
|
-
catch (exc) {
|
|
891
|
-
return {
|
|
892
|
-
status: AgentStatus.ERROR,
|
|
893
|
-
response: {
|
|
894
|
-
status: AgentStatus.ERROR,
|
|
895
|
-
prompt: null,
|
|
896
|
-
error: `loop_compile failed: ${exc}`,
|
|
897
|
-
},
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
// Persist lineage to vault
|
|
901
|
-
if (options.persistLineage !== false) {
|
|
902
|
-
this.persistLoopLineage(response, lcr);
|
|
903
|
-
}
|
|
904
|
-
return {
|
|
905
|
-
status: AgentStatus.OK,
|
|
906
|
-
response: {
|
|
907
|
-
status: AgentStatus.OK,
|
|
908
|
-
prompt: response.prompt,
|
|
909
|
-
error: null,
|
|
910
|
-
state_file_content: response.state_file_content,
|
|
911
|
-
prompt_artifact: response.prompt_artifact,
|
|
912
|
-
warnings: response.warnings,
|
|
913
|
-
// v2.12: Pass the compiler's derived state through for typed
|
|
914
|
-
// projections (consumed by getProjection without re-persisting).
|
|
915
|
-
loop_objective: response.loop_objective,
|
|
916
|
-
rolling_summary: response.rolling_summary,
|
|
917
|
-
sub_goals: response.sub_goals,
|
|
918
|
-
criterion_statuses: response.criterion_statuses,
|
|
919
|
-
},
|
|
920
|
-
};
|
|
921
62
|
}
|
|
922
63
|
}
|
|
923
64
|
// ═══════════════════════════════════════════════════════════════════════════
|