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.
- package/README.md +128 -145
- package/dist/audit.d.ts +50 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +158 -0
- package/dist/audit.js.map +1 -0
- package/dist/canonical-state.d.ts +28 -20
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +82 -173
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +25 -16
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +25 -33
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +38 -138
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/enforcement-gate.d.ts +76 -0
- package/dist/enforcement-gate.d.ts.map +1 -0
- package/dist/enforcement-gate.js +1071 -0
- package/dist/enforcement-gate.js.map +1 -0
- package/dist/engine.d.ts +99 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +839 -0
- package/dist/engine.js.map +1 -0
- package/dist/evidence-claims.d.ts +50 -0
- package/dist/evidence-claims.d.ts.map +1 -0
- package/dist/evidence-claims.js +134 -0
- package/dist/evidence-claims.js.map +1 -0
- package/dist/evidence-provider.d.ts +21 -23
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +74 -34
- package/dist/evidence-provider.js.map +1 -1
- package/dist/generate-schema.js +1 -84
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +19 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -15
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +41 -3
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +632 -273
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +2 -2
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +2 -2
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts +26 -0
- package/dist/loop-projection.d.ts.map +1 -0
- package/dist/loop-projection.js +142 -0
- package/dist/loop-projection.js.map +1 -0
- package/dist/loop-store.d.ts +112 -24
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +365 -218
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +216 -0
- package/dist/mcp/round-lifecycle.d.ts.map +1 -0
- package/dist/mcp/round-lifecycle.js +926 -0
- package/dist/mcp/round-lifecycle.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +28 -42
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +53 -101
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +306 -1105
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +13 -16
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +479 -413
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +12 -17
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +95 -161
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +44 -35
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +31 -115
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +37 -6
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +389 -160
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -8
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +8 -26
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +357 -229
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +139 -15
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +5 -13
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +19 -55
- package/dist/replay.js.map +1 -1
- package/dist/round-coordinator.d.ts +20 -24
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +99 -92
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +14 -13
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +26 -14
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +15 -18
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +38 -47
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +53 -0
- package/dist/self-eval.d.ts.map +1 -0
- package/dist/self-eval.js +284 -0
- package/dist/self-eval.js.map +1 -0
- package/dist/storage.d.ts +9 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +18 -6
- package/dist/storage.js.map +1 -1
- package/dist/token-utils.d.ts +15 -3
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +24 -5
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +108 -0
- package/dist/verification-gate.d.ts.map +1 -0
- package/dist/verification-gate.js +971 -0
- package/dist/verification-gate.js.map +1 -0
- package/loop_policy.json +44 -15
- package/package.json +5 -10
- package/skills/perception/SKILL.md +182 -199
- package/dist/canonical-event-writer.d.ts +0 -20
- package/dist/canonical-event-writer.d.ts.map +0 -1
- package/dist/canonical-event-writer.js +0 -131
- package/dist/canonical-event-writer.js.map +0 -1
- package/dist/compiler-subgoals.d.ts +0 -7
- package/dist/compiler-subgoals.d.ts.map +0 -1
- package/dist/compiler-subgoals.js +0 -93
- package/dist/compiler-subgoals.js.map +0 -1
- package/dist/derive-id.d.ts +0 -12
- package/dist/derive-id.d.ts.map +0 -1
- package/dist/derive-id.js +0 -31
- package/dist/derive-id.js.map +0 -1
- package/dist/mcp/session-projection.d.ts +0 -5
- package/dist/mcp/session-projection.d.ts.map +0 -1
- package/dist/mcp/session-projection.js +0 -18
- package/dist/mcp/session-projection.js.map +0 -1
- package/dist/mcp/session-queue.d.ts +0 -3
- package/dist/mcp/session-queue.d.ts.map +0 -1
- package/dist/mcp/session-queue.js +0 -18
- package/dist/mcp/session-queue.js.map +0 -1
- package/dist/mcp/session-types.d.ts +0 -72
- package/dist/mcp/session-types.d.ts.map +0 -1
- package/dist/mcp/session-types.js +0 -2
- package/dist/mcp/session-types.js.map +0 -1
- package/dist/round-commit-store.d.ts +0 -12
- package/dist/round-commit-store.d.ts.map +0 -1
- package/dist/round-commit-store.js +0 -13
- package/dist/round-commit-store.js.map +0 -1
- package/dist/round-gate.d.ts +0 -31
- package/dist/round-gate.d.ts.map +0 -1
- package/dist/round-gate.js +0 -259
- package/dist/round-gate.js.map +0 -1
- package/dist/round-recovery.d.ts +0 -17
- package/dist/round-recovery.d.ts.map +0 -1
- package/dist/round-recovery.js +0 -117
- package/dist/round-recovery.js.map +0 -1
- package/dist/round-report.d.ts +0 -16
- package/dist/round-report.d.ts.map +0 -1
- package/dist/round-report.js +0 -401
- package/dist/round-report.js.map +0 -1
- package/dist/runtime-services.d.ts +0 -59
- package/dist/runtime-services.d.ts.map +0 -1
- package/dist/runtime-services.js +0 -460
- package/dist/runtime-services.js.map +0 -1
- package/dist/vault-format.d.ts +0 -4
- package/dist/vault-format.d.ts.map +0 -1
- package/dist/vault-format.js +0 -4
- package/dist/vault-format.js.map +0 -1
- package/dist/vault-types.d.ts +0 -24
- package/dist/vault-types.d.ts.map +0 -1
- package/dist/vault-types.js +0 -2
- package/dist/vault-types.js.map +0 -1
- 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
|