loopforge 2.0.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/canonical-state.d.ts +23 -2
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +90 -6
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -1
- package/dist/cli.js.map +1 -1
- package/dist/enforcement-gate.d.ts +1 -1
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +86 -7
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +2 -10
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +56 -106
- package/dist/engine.js.map +1 -1
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +1 -8
- package/dist/evidence-provider.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +511 -48
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +23 -2
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/session.d.ts +2 -4
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +27 -50
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +32 -5
- package/dist/mcp/tools.js.map +1 -1
- package/dist/observability.d.ts +5 -36
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +9 -79
- package/dist/observability.js.map +1 -1
- package/dist/policy-metrics.d.ts +1 -0
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +2 -0
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +58 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +24 -3
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +104 -0
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/protocol.d.ts +115 -12
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +40 -4
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +0 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +1 -87
- package/dist/replay.js.map +1 -1
- package/dist/round-coordinator.d.ts +0 -2
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +71 -76
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +1 -0
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +2 -0
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +2 -22
- package/dist/round-transaction.js.map +1 -1
- package/dist/run.d.ts +51 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +200 -0
- package/dist/run.js.map +1 -0
- package/dist/self-eval.d.ts +0 -3
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +15 -18
- package/dist/self-eval.js.map +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +13 -3
- package/dist/storage.js.map +1 -1
- package/dist/token-utils.d.ts +22 -0
- package/dist/token-utils.d.ts.map +1 -0
- package/dist/token-utils.js +46 -0
- package/dist/token-utils.js.map +1 -0
- package/dist/verification-gate.d.ts +1 -5
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +109 -58
- package/dist/verification-gate.js.map +1 -1
- package/loop_policy.json +20 -3
- package/package.json +2 -2
package/dist/loop-compiler.js
CHANGED
|
@@ -5,16 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { getPolicy } from "./policy.js";
|
|
8
|
-
import { AgentStatus,
|
|
8
|
+
import { AgentStatus, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeConstraintMeta, makeMilestoneSummary, makeRollingSummary, makeSubGoal, makeTaskAlignment, } from "./protocol.js";
|
|
9
9
|
import { createCanonicalLoopState, renderCanonicalStateMarkdown, } from "./canonical-state.js";
|
|
10
10
|
import { assemblePromptArtifact } from "./prompt-assembler.js";
|
|
11
11
|
import { decidePromptLevel, } from "./prompt-policy.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.filter((value) => typeof value === "string")
|
|
15
|
-
.map((value) => value.trim())
|
|
16
|
-
.filter(Boolean))];
|
|
17
|
-
}
|
|
12
|
+
import { jaccardSimilarity, unique, entryRound } from "./token-utils.js";
|
|
13
|
+
// unique() imported from token-utils.ts
|
|
18
14
|
function contextEntries(context) {
|
|
19
15
|
if (!context || !Array.isArray(context.results))
|
|
20
16
|
return [];
|
|
@@ -26,10 +22,7 @@ function lineage(entry) {
|
|
|
26
22
|
? value
|
|
27
23
|
: {};
|
|
28
24
|
}
|
|
29
|
-
|
|
30
|
-
const value = lineage(entry).round;
|
|
31
|
-
return typeof value === "number" ? value : 0;
|
|
32
|
-
}
|
|
25
|
+
// entryRound() imported from token-utils.ts
|
|
33
26
|
function loopEntries(loopId, context) {
|
|
34
27
|
return contextEntries(context)
|
|
35
28
|
.filter((entry) => entry.loop_id === loopId || lineage(entry).loop_id === loopId)
|
|
@@ -139,19 +132,111 @@ function evolveConstraints(request, objective, previous) {
|
|
|
139
132
|
.slice(0, getPolicy().evolution.max_active_constraints);
|
|
140
133
|
return { active, retired };
|
|
141
134
|
}
|
|
135
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
136
|
+
// Hierarchical Summary (v2.1)
|
|
137
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
138
|
+
/** Extract execution_evidence from a vault entry, handling both direct-field
|
|
139
|
+
* and lineage-nested shapes. Returns null when absent. */
|
|
140
|
+
function entryExecutionEvidence(entry) {
|
|
141
|
+
const direct = entry.execution_evidence;
|
|
142
|
+
if (direct && typeof direct === "object" && !Array.isArray(direct)) {
|
|
143
|
+
return direct;
|
|
144
|
+
}
|
|
145
|
+
const lin = lineage(entry);
|
|
146
|
+
const nested = lin.execution_evidence;
|
|
147
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
148
|
+
return nested;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
/** Read success_criteria_met from an entry's execution_evidence. */
|
|
153
|
+
function entryCriteriaMet(entry) {
|
|
154
|
+
const ev = entryExecutionEvidence(entry);
|
|
155
|
+
if (!ev)
|
|
156
|
+
return [];
|
|
157
|
+
const arr = ev.success_criteria_met;
|
|
158
|
+
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
159
|
+
}
|
|
160
|
+
/** Detect newly met criteria by comparing the current entry's
|
|
161
|
+
* success_criteria_met against the previous entry's, using Jaccard
|
|
162
|
+
* similarity with threshold 0.7 to avoid Surface-form mismatches.
|
|
163
|
+
* Returns empty array when there is no previous entry — the first
|
|
164
|
+
* entry's criteria are the baseline, not a "new" event. */
|
|
165
|
+
function detectNewCriteria(current, previous) {
|
|
166
|
+
const currMet = entryCriteriaMet(current);
|
|
167
|
+
if (currMet.length === 0)
|
|
168
|
+
return [];
|
|
169
|
+
if (!previous) {
|
|
170
|
+
// First round with criteria is the baseline — not a milestone trigger.
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
const prevMet = entryCriteriaMet(previous);
|
|
174
|
+
if (prevMet.length === 0)
|
|
175
|
+
return currMet;
|
|
176
|
+
return currMet.filter((curr) => prevMet.every((prev) => jaccardSimilarity(curr, prev) < getPolicy().evolution.criteria_dedup_threshold));
|
|
177
|
+
}
|
|
178
|
+
/** Read constraints_active from an entry's lineage. */
|
|
179
|
+
function entryActiveConstraints(entry) {
|
|
180
|
+
const lin = lineage(entry);
|
|
181
|
+
const arr = lin.constraints_active;
|
|
182
|
+
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
183
|
+
}
|
|
184
|
+
/** Read retracted_constraints from an entry. */
|
|
185
|
+
function entryRetractedConstraints(entry) {
|
|
186
|
+
const direct = entry.retracted_constraints;
|
|
187
|
+
if (Array.isArray(direct))
|
|
188
|
+
return direct.filter((v) => typeof v === "string");
|
|
189
|
+
const lin = lineage(entry);
|
|
190
|
+
const nested = lin.retracted_constraints;
|
|
191
|
+
if (Array.isArray(nested))
|
|
192
|
+
return nested.filter((v) => typeof v === "string");
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
/** Read progress_estimate from an entry's execution_evidence. */
|
|
196
|
+
function entryProgressEstimate(entry) {
|
|
197
|
+
const ev = entryExecutionEvidence(entry);
|
|
198
|
+
if (!ev)
|
|
199
|
+
return 0;
|
|
200
|
+
const pe = ev.progress_estimate;
|
|
201
|
+
return typeof pe === "number" ? pe : 0;
|
|
202
|
+
}
|
|
203
|
+
/** Build a single MilestoneSummary from a range of completed round entries. */
|
|
204
|
+
function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, kind) {
|
|
205
|
+
const last = phaseEntries.at(-1);
|
|
206
|
+
const outcomeRaw = last
|
|
207
|
+
? (typeof last.output_summary === "string" ? last.output_summary : "")
|
|
208
|
+
: "";
|
|
209
|
+
const outcomeMax = getPolicy().checkpoint.outcome_max_chars;
|
|
210
|
+
const outcome = outcomeRaw.length > outcomeMax
|
|
211
|
+
? outcomeRaw.slice(0, outcomeMax) + "…"
|
|
212
|
+
: outcomeRaw;
|
|
213
|
+
const carried = last ? entryActiveConstraints(last) : [];
|
|
214
|
+
const resolved = unique(phaseEntries.flatMap((e) => entryRetractedConstraints(e)));
|
|
215
|
+
const progress = last ? entryProgressEstimate(last) : 0;
|
|
216
|
+
return makeMilestoneSummary({
|
|
217
|
+
label,
|
|
218
|
+
round_range: { start: startRound, end: endRound },
|
|
219
|
+
outcome,
|
|
220
|
+
carried_constraints: carried,
|
|
221
|
+
resolved_constraints: resolved,
|
|
222
|
+
progress_at_boundary: progress,
|
|
223
|
+
kind,
|
|
224
|
+
generated_at_round: endRound,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
142
227
|
export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0) {
|
|
143
|
-
const
|
|
144
|
-
const
|
|
228
|
+
const policy = getPolicy().summary;
|
|
229
|
+
const allEntries = loopEntries(loopId, context);
|
|
230
|
+
// ── Phase 1: Recent Window (preserves existing behavior) ──
|
|
231
|
+
const windowEntries = allEntries
|
|
145
232
|
.filter((entry) => {
|
|
146
233
|
const round = entryRound(entry);
|
|
147
234
|
return round >= sinceRound && round < currentRound;
|
|
148
235
|
})
|
|
149
|
-
.slice(-window);
|
|
150
|
-
if (entries.length === 0)
|
|
151
|
-
return null;
|
|
236
|
+
.slice(-policy.window);
|
|
152
237
|
const outcomes = [];
|
|
153
238
|
const issues = [];
|
|
154
|
-
for (const entry of
|
|
239
|
+
for (const entry of windowEntries) {
|
|
155
240
|
const data = lineage(entry);
|
|
156
241
|
const round = entryRound(entry);
|
|
157
242
|
const success = entry.success ?? data.success;
|
|
@@ -165,33 +250,387 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
165
250
|
: Array.isArray(data.constraint_violations) ? data.constraint_violations : [];
|
|
166
251
|
issues.push(...violations.filter((item) => typeof item === "string"));
|
|
167
252
|
}
|
|
253
|
+
// ── Phase 2: Milestone Accumulation ──
|
|
254
|
+
// Scan all completed entries in round order. For each entry, check three
|
|
255
|
+
// trigger signals in priority order: agent_declared > criteria_milestone > auto.
|
|
256
|
+
const completedEntries = allEntries.filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
|
|
257
|
+
const milestones = [];
|
|
258
|
+
let lastMilestoneRound = 0;
|
|
259
|
+
for (let i = 0; i < completedEntries.length; i++) {
|
|
260
|
+
const entry = completedEntries[i];
|
|
261
|
+
const rnd = entryRound(entry);
|
|
262
|
+
const lin = lineage(entry);
|
|
263
|
+
// Signal 1: Agent-declared checkpoint
|
|
264
|
+
const isAgentCheckpoint = lin.compression_checkpoint === true;
|
|
265
|
+
// Signal 2: New criteria met (compare with previous vault entry)
|
|
266
|
+
const prev = i > 0 ? completedEntries[i - 1] : null;
|
|
267
|
+
const newCriteria = detectNewCriteria(entry, prev);
|
|
268
|
+
if (isAgentCheckpoint || newCriteria.length > 0) {
|
|
269
|
+
const startRound = lastMilestoneRound + 1;
|
|
270
|
+
const phaseEntries = completedEntries.filter((e) => {
|
|
271
|
+
const er = entryRound(e);
|
|
272
|
+
return er >= startRound && er <= rnd;
|
|
273
|
+
});
|
|
274
|
+
const kind = isAgentCheckpoint
|
|
275
|
+
? "agent_declared"
|
|
276
|
+
: "criteria_milestone";
|
|
277
|
+
const label = isAgentCheckpoint
|
|
278
|
+
? (typeof lin.checkpoint_label === "string" && lin.checkpoint_label.trim()
|
|
279
|
+
? lin.checkpoint_label.trim()
|
|
280
|
+
: `Round ${rnd}`)
|
|
281
|
+
: `Completed: ${newCriteria.join(", ")}`;
|
|
282
|
+
milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, rnd, label, kind));
|
|
283
|
+
lastMilestoneRound = rnd;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Signal 3: Safety net — auto milestone when gap exceeds milestone_interval
|
|
287
|
+
const gapSinceLast = (currentRound - 1) - lastMilestoneRound;
|
|
288
|
+
if (lastMilestoneRound > 0 &&
|
|
289
|
+
gapSinceLast >= policy.milestone_interval &&
|
|
290
|
+
completedEntries.length > 0) {
|
|
291
|
+
const startRound = lastMilestoneRound + 1;
|
|
292
|
+
const endRound = currentRound - 1;
|
|
293
|
+
const phaseEntries = completedEntries.filter((e) => {
|
|
294
|
+
const er = entryRound(e);
|
|
295
|
+
return er >= startRound && er <= endRound;
|
|
296
|
+
});
|
|
297
|
+
if (phaseEntries.length > 0) {
|
|
298
|
+
milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, endRound, `Rounds ${startRound}–${endRound}`, "auto"));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const cappedMilestones = milestones.slice(-policy.max_milestones);
|
|
302
|
+
// ── Phase 3: Loop Synthesis (formulaic) ──
|
|
303
|
+
let loopSynthesis = "";
|
|
304
|
+
if (policy.enable_loop_synthesis && currentRound > 1) {
|
|
305
|
+
const totalRounds = currentRound - 1;
|
|
306
|
+
const phaseCount = cappedMilestones.length;
|
|
307
|
+
const lastProgress = completedEntries.length > 0
|
|
308
|
+
? entryProgressEstimate(completedEntries[completedEntries.length - 1])
|
|
309
|
+
: 0;
|
|
310
|
+
const activeCount = completedEntries.length > 0
|
|
311
|
+
? entryActiveConstraints(completedEntries[completedEntries.length - 1]).length
|
|
312
|
+
: 0;
|
|
313
|
+
const totalResolved = cappedMilestones.reduce((sum, m) => sum + m.resolved_constraints.length, 0);
|
|
314
|
+
loopSynthesis =
|
|
315
|
+
`Loop spans ${totalRounds} rounds across ${phaseCount} phases. ` +
|
|
316
|
+
`Overall progress: ${(lastProgress * 100).toFixed(0)}%. ` +
|
|
317
|
+
`${activeCount} active constraints, ${totalResolved} resolved across all phases.`;
|
|
318
|
+
}
|
|
319
|
+
if (windowEntries.length === 0 && cappedMilestones.length === 0)
|
|
320
|
+
return null;
|
|
168
321
|
return makeRollingSummary({
|
|
169
322
|
key_outcomes: unique(outcomes),
|
|
170
323
|
recurring_issues: unique(issues),
|
|
171
|
-
rounds_sampled:
|
|
324
|
+
rounds_sampled: windowEntries.length,
|
|
172
325
|
generated_at_round: currentRound,
|
|
173
326
|
failed_patterns: [],
|
|
327
|
+
milestones: cappedMilestones,
|
|
328
|
+
loop_synthesis: loopSynthesis || undefined,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
332
|
+
// Sub-Goal Structured Tracking (v2.2)
|
|
333
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
334
|
+
/** Derive a stable sub-goal ID from its description hash. */
|
|
335
|
+
function deriveSubGoalId(description) {
|
|
336
|
+
return "sg-" + computeGoalTextHash(description).slice(0, 8);
|
|
337
|
+
}
|
|
338
|
+
/** Read emerged_subtasks from a vault entry (handles direct + lineage nesting). */
|
|
339
|
+
function entryEmergedSubtasks(entry) {
|
|
340
|
+
const direct = entry.emerged_subtasks;
|
|
341
|
+
if (Array.isArray(direct))
|
|
342
|
+
return direct.filter((v) => typeof v === "string");
|
|
343
|
+
const lin = lineage(entry);
|
|
344
|
+
const nested = lin.emerged_subtasks;
|
|
345
|
+
if (Array.isArray(nested))
|
|
346
|
+
return nested.filter((v) => typeof v === "string");
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
/** Read completed_subtasks from an entry. */
|
|
350
|
+
function entryCompletedSubtasks(entry) {
|
|
351
|
+
const direct = entry.completed_subtasks;
|
|
352
|
+
if (Array.isArray(direct))
|
|
353
|
+
return direct.filter((v) => typeof v === "string");
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
/** Read blocked_subtasks from an entry. */
|
|
357
|
+
function entryBlockedSubtasks(entry) {
|
|
358
|
+
const direct = entry.blocked_subtasks;
|
|
359
|
+
if (Array.isArray(direct))
|
|
360
|
+
return direct.filter((v) => typeof v === "string");
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
/** Read canceled_subtasks from an entry. */
|
|
364
|
+
function entryCanceledSubtasks(entry) {
|
|
365
|
+
const direct = entry.canceled_subtasks;
|
|
366
|
+
if (Array.isArray(direct))
|
|
367
|
+
return direct.filter((v) => typeof v === "string");
|
|
368
|
+
return [];
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Match a user-provided sub-task description against existing SubGoals.
|
|
372
|
+
* Returns the index of the best-matching SubGoal with similarity >= threshold,
|
|
373
|
+
* or -1 if no match found.
|
|
374
|
+
*/
|
|
375
|
+
function matchSubGoal(target, subGoals, threshold) {
|
|
376
|
+
let bestIdx = -1;
|
|
377
|
+
let bestScore = 0;
|
|
378
|
+
for (let i = 0; i < subGoals.length; i++) {
|
|
379
|
+
const score = jaccardSimilarity(target, subGoals[i].description);
|
|
380
|
+
if (score > bestScore) {
|
|
381
|
+
bestScore = score;
|
|
382
|
+
bestIdx = i;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return bestScore >= threshold ? bestIdx : -1;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Manage the full sub-goal lifecycle across rounds.
|
|
389
|
+
*
|
|
390
|
+
* Accumulates sub-goals from all prior vault entries, processes the current
|
|
391
|
+
* round's declarations, derives status changes, and returns the complete
|
|
392
|
+
* structured SubGoal list.
|
|
393
|
+
*
|
|
394
|
+
* Status derivation priority (compiler-driven):
|
|
395
|
+
* 1. Agent declared done/canceled/blocked → explicit status change
|
|
396
|
+
* 2. next_action match → auto in_progress
|
|
397
|
+
* 3. success_criteria_met match → auto done
|
|
398
|
+
*/
|
|
399
|
+
function manageSubGoals(loopId, currentRound, lastRoundResult, vaultContext) {
|
|
400
|
+
const policy = getPolicy().evolution;
|
|
401
|
+
const allEntries = loopEntries(loopId, vaultContext);
|
|
402
|
+
const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
|
|
403
|
+
const subGoals = [];
|
|
404
|
+
// Phase 1 — Accumulate historical sub-goals from vault entries
|
|
405
|
+
for (const entry of completedEntries) {
|
|
406
|
+
const rnd = entryRound(entry);
|
|
407
|
+
const emerged = entryEmergedSubtasks(entry);
|
|
408
|
+
for (const desc of emerged) {
|
|
409
|
+
const id = deriveSubGoalId(desc);
|
|
410
|
+
// Dedup: skip if an existing sub-goal is too similar
|
|
411
|
+
if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
|
|
412
|
+
continue;
|
|
413
|
+
subGoals.push(makeSubGoal({
|
|
414
|
+
id,
|
|
415
|
+
description: desc.trim(),
|
|
416
|
+
status: "pending",
|
|
417
|
+
declared_at_round: rnd,
|
|
418
|
+
status_changed_at_round: rnd,
|
|
419
|
+
priority: subGoals.length,
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// Phase 2 — Process current round's sub-task declarations
|
|
424
|
+
if (lastRoundResult) {
|
|
425
|
+
const rnd = lastRoundResult.round || (currentRound - 1);
|
|
426
|
+
// New sub-goals from this round
|
|
427
|
+
const emerged = lastRoundResult.emerged_subtasks ?? [];
|
|
428
|
+
for (const desc of emerged) {
|
|
429
|
+
if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
|
|
430
|
+
continue;
|
|
431
|
+
subGoals.push(makeSubGoal({
|
|
432
|
+
id: deriveSubGoalId(desc),
|
|
433
|
+
description: desc.trim(),
|
|
434
|
+
status: "pending",
|
|
435
|
+
declared_at_round: rnd,
|
|
436
|
+
status_changed_at_round: rnd,
|
|
437
|
+
priority: subGoals.length,
|
|
438
|
+
}));
|
|
439
|
+
}
|
|
440
|
+
// Completed sub-goals
|
|
441
|
+
const completed = lastRoundResult.completed_subtasks ?? [];
|
|
442
|
+
for (const desc of completed) {
|
|
443
|
+
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
444
|
+
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
445
|
+
subGoals[idx].status = "done";
|
|
446
|
+
subGoals[idx].status_changed_at_round = rnd;
|
|
447
|
+
subGoals[idx].completed_at_round = rnd;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Blocked sub-goals
|
|
451
|
+
const blocked = lastRoundResult.blocked_subtasks ?? [];
|
|
452
|
+
for (const desc of blocked) {
|
|
453
|
+
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
454
|
+
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
455
|
+
subGoals[idx].status = "blocked";
|
|
456
|
+
subGoals[idx].status_changed_at_round = rnd;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// Canceled sub-goals
|
|
460
|
+
const canceled = lastRoundResult.canceled_subtasks ?? [];
|
|
461
|
+
for (const desc of canceled) {
|
|
462
|
+
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
463
|
+
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
464
|
+
subGoals[idx].status = "canceled";
|
|
465
|
+
subGoals[idx].status_changed_at_round = rnd;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// Phase 3 — Auto-derive: next_action match → in_progress
|
|
469
|
+
const nextAction = lastRoundResult.next_action?.trim();
|
|
470
|
+
if (nextAction) {
|
|
471
|
+
for (const sg of subGoals) {
|
|
472
|
+
if (sg.status === "pending") {
|
|
473
|
+
if (jaccardSimilarity(nextAction, sg.description) >= policy.subgoal_auto_in_progress_threshold) {
|
|
474
|
+
sg.status = "in_progress";
|
|
475
|
+
sg.status_changed_at_round = rnd;
|
|
476
|
+
break; // Only one sub-goal can be in_progress at a time
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// Phase 4 — Auto-complete: new criteria match → done
|
|
482
|
+
const newCriteria = lastRoundResult.execution_evidence?.success_criteria_met ?? [];
|
|
483
|
+
if (newCriteria.length > 0) {
|
|
484
|
+
for (const sg of subGoals) {
|
|
485
|
+
if (sg.status === "pending" || sg.status === "in_progress") {
|
|
486
|
+
for (const crit of newCriteria) {
|
|
487
|
+
if (jaccardSimilarity(crit, sg.description) >= policy.subgoal_auto_complete_threshold) {
|
|
488
|
+
sg.status = "done";
|
|
489
|
+
sg.status_changed_at_round = rnd;
|
|
490
|
+
sg.completed_at_round = rnd;
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Sort for display: in_progress first, then pending by age desc, then blocked, then done, then canceled
|
|
499
|
+
subGoals.sort((a, b) => {
|
|
500
|
+
const rank = (s) => {
|
|
501
|
+
switch (s.status) {
|
|
502
|
+
case "in_progress": return 0;
|
|
503
|
+
case "pending": return 1;
|
|
504
|
+
case "blocked": return 2;
|
|
505
|
+
case "done": return 3;
|
|
506
|
+
case "canceled": return 4;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
const ra = rank(a), rb = rank(b);
|
|
510
|
+
if (ra !== rb)
|
|
511
|
+
return ra - rb;
|
|
512
|
+
// Within same status: pending by oldest first, done by most recent first
|
|
513
|
+
if (a.status === "pending")
|
|
514
|
+
return (currentRound - a.declared_at_round) - (currentRound - b.declared_at_round);
|
|
515
|
+
if (a.status === "done")
|
|
516
|
+
return (b.completed_at_round ?? 0) - (a.completed_at_round ?? 0);
|
|
517
|
+
return a.declared_at_round - b.declared_at_round;
|
|
174
518
|
});
|
|
519
|
+
return subGoals;
|
|
175
520
|
}
|
|
176
|
-
|
|
177
|
-
|
|
521
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
522
|
+
// Time-Aware Constraint Management (v2.3)
|
|
523
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
524
|
+
/** Determine the source of a constraint by matching against known sources. */
|
|
525
|
+
function constraintSource(text, objective, constraintsFromPlan) {
|
|
526
|
+
const norm = text.toLowerCase().trim();
|
|
527
|
+
if (objective.hard_constraints.some((c) => c.toLowerCase().trim() === norm))
|
|
528
|
+
return "hard";
|
|
529
|
+
if (constraintsFromPlan.some((c) => c.toLowerCase().trim() === norm))
|
|
530
|
+
return "plan";
|
|
531
|
+
if (objective.success_criteria.some((c) => c.toLowerCase().trim() === norm))
|
|
532
|
+
return "criteria";
|
|
533
|
+
return "discovered";
|
|
178
534
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
535
|
+
/** Find the earliest round a constraint was discovered by scanning vault entries.
|
|
536
|
+
* Uses similarity ≥ 0.5 for fuzzy matching. */
|
|
537
|
+
function findDiscoveredRound(text, vaultEntries) {
|
|
538
|
+
let earliest = 0;
|
|
539
|
+
for (const entry of vaultEntries) {
|
|
540
|
+
const rnd = entryRound(entry);
|
|
541
|
+
if (rnd < 1)
|
|
542
|
+
continue;
|
|
543
|
+
const discovered = Array.isArray(entry.discovered_constraints)
|
|
544
|
+
? entry.discovered_constraints.filter((v) => typeof v === "string")
|
|
545
|
+
: [];
|
|
546
|
+
for (const d of discovered) {
|
|
547
|
+
if (jaccardSimilarity(text, d) >= getPolicy().evolution.constraint_match_threshold) {
|
|
548
|
+
if (earliest === 0 || rnd < earliest)
|
|
549
|
+
earliest = rnd;
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return earliest;
|
|
189
555
|
}
|
|
556
|
+
/** Find the latest round a constraint was violated by scanning vault entries.
|
|
557
|
+
* Uses similarity ≥ 0.5 for fuzzy matching. Returns 0 if never violated. */
|
|
558
|
+
function findLastViolatedRound(text, vaultEntries) {
|
|
559
|
+
let latest = 0;
|
|
560
|
+
for (const entry of vaultEntries) {
|
|
561
|
+
const rnd = entryRound(entry);
|
|
562
|
+
if (rnd < 1)
|
|
563
|
+
continue;
|
|
564
|
+
const violations = Array.isArray(entry.constraint_violations)
|
|
565
|
+
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
566
|
+
: [];
|
|
567
|
+
for (const v of violations) {
|
|
568
|
+
if (jaccardSimilarity(text, v) >= getPolicy().evolution.constraint_match_threshold) {
|
|
569
|
+
if (rnd > latest)
|
|
570
|
+
latest = rnd;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return latest;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Manage constraint lifecycle with time-aware decay.
|
|
579
|
+
*
|
|
580
|
+
* Discovered constraints that haven't been violated for N rounds are demoted
|
|
581
|
+
* to inactive — removed from prompts but kept in state. Hard/plan/criteria
|
|
582
|
+
* constraints never auto-decay. Inactive constraints are auto-reactivated
|
|
583
|
+
* if violated again in the current round.
|
|
584
|
+
*/
|
|
585
|
+
function manageConstraintLifecycle(active, objective, constraintsFromPlan, vaultContext, currentRound) {
|
|
586
|
+
const policy = getPolicy().evolution;
|
|
587
|
+
const allEntries = loopEntries(objective.loop_id || "", vaultContext);
|
|
588
|
+
const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
|
|
589
|
+
const metadata = [];
|
|
590
|
+
for (const text of active) {
|
|
591
|
+
const source = constraintSource(text, objective, constraintsFromPlan);
|
|
592
|
+
// Structural constraints never decay
|
|
593
|
+
if (source === "hard" || source === "plan" || source === "criteria") {
|
|
594
|
+
metadata.push(makeConstraintMeta({
|
|
595
|
+
text,
|
|
596
|
+
discovered_at_round: objective.created_at_round || 1,
|
|
597
|
+
last_violated_at_round: findLastViolatedRound(text, completedEntries),
|
|
598
|
+
source,
|
|
599
|
+
status: "active",
|
|
600
|
+
}));
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
// Discovered constraints: check for inactivity
|
|
604
|
+
const discoveredAt = findDiscoveredRound(text, completedEntries);
|
|
605
|
+
const lastViolated = findLastViolatedRound(text, completedEntries);
|
|
606
|
+
const roundsSinceRelevant = lastViolated > 0
|
|
607
|
+
? currentRound - lastViolated
|
|
608
|
+
: currentRound - (discoveredAt || currentRound);
|
|
609
|
+
const status = roundsSinceRelevant > policy.constraint_inactive_rounds
|
|
610
|
+
? "inactive"
|
|
611
|
+
: "active";
|
|
612
|
+
metadata.push(makeConstraintMeta({
|
|
613
|
+
text,
|
|
614
|
+
discovered_at_round: discoveredAt || 0,
|
|
615
|
+
last_violated_at_round: lastViolated,
|
|
616
|
+
source: "discovered",
|
|
617
|
+
status,
|
|
618
|
+
}));
|
|
619
|
+
}
|
|
620
|
+
const newActive = metadata
|
|
621
|
+
.filter((m) => m.status === "active")
|
|
622
|
+
.map((m) => m.text);
|
|
623
|
+
const inactive = metadata
|
|
624
|
+
.filter((m) => m.status === "inactive")
|
|
625
|
+
.map((m) => m.text);
|
|
626
|
+
return { active: newActive, inactive, metadata };
|
|
627
|
+
}
|
|
628
|
+
// jaccardSimilarity() imported from token-utils.ts
|
|
190
629
|
export function alignTask(proposedTask, request, context) {
|
|
191
630
|
const objective = request.loop_objective ?? latestObjective(request.loop_id, context);
|
|
192
631
|
if (!objective)
|
|
193
632
|
return makeTaskAlignment();
|
|
194
|
-
const score =
|
|
633
|
+
const score = jaccardSimilarity(proposedTask, [objective.objective, ...objective.success_criteria, ...objective.hard_constraints].join(" "));
|
|
195
634
|
return makeTaskAlignment({
|
|
196
635
|
is_aligned: score >= 0.3,
|
|
197
636
|
alignment_score: Number(score.toFixed(2)),
|
|
@@ -204,7 +643,7 @@ export function checkLoopHealth(loopId, request, context) {
|
|
|
204
643
|
const alignment = alignTask(request.task, request, context);
|
|
205
644
|
const violations = request.last_round_result?.constraint_violations.length ?? 0;
|
|
206
645
|
const integrity = Math.max(0, 1 - violations * 0.2);
|
|
207
|
-
const continuity = previous ?
|
|
646
|
+
const continuity = previous ? jaccardSimilarity(previous.task, request.task) : 1;
|
|
208
647
|
const drift = alignment.alignment_score < 0.3 || continuity < 0.2;
|
|
209
648
|
return makeLoopHealth({
|
|
210
649
|
goal_alignment: alignment.alignment_score,
|
|
@@ -272,6 +711,9 @@ export function buildSelfEvalBlock(round) {
|
|
|
272
711
|
},
|
|
273
712
|
wrong_assumptions: [],
|
|
274
713
|
next_action: "<next concrete action, or empty when complete>",
|
|
714
|
+
completed_subtasks: [],
|
|
715
|
+
blocked_subtasks: [],
|
|
716
|
+
canceled_subtasks: [],
|
|
275
717
|
}, null, 2),
|
|
276
718
|
"---end-loopforge-eval",
|
|
277
719
|
"",
|
|
@@ -279,25 +721,17 @@ export function buildSelfEvalBlock(round) {
|
|
|
279
721
|
"Set success=true only when the full goal and ALL hard constraints are verified.",
|
|
280
722
|
].join("\n");
|
|
281
723
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return null;
|
|
286
|
-
const policy = getPolicy().checkpoint;
|
|
287
|
-
return makeCheckpointSummary({
|
|
288
|
-
label: last.checkpoint_label?.trim() || `Round ${last.round}`,
|
|
289
|
-
declared_at_round: last.round,
|
|
290
|
-
outcome: last.output_summary.slice(0, policy.outcome_max_chars),
|
|
291
|
-
carried_constraints: active.slice(0, policy.max_carried_constraints),
|
|
292
|
-
resolved_constraints: retired,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
724
|
+
// v2.5: checkpoint() removed — agent_declared milestones from
|
|
725
|
+
// compression_checkpoint supersede the separate CheckpointSummary.
|
|
726
|
+
// See buildRollingSummary() signal 1.
|
|
295
727
|
export function compileLoop(request, context) {
|
|
296
728
|
const decision = levelDecision(request, context);
|
|
297
729
|
const previous = getPreviousRound(request.loop_id, request.round - 1, context);
|
|
298
730
|
const objective = evolveObjective(request, context);
|
|
299
731
|
const constraints = evolveConstraints(request, objective, previous);
|
|
732
|
+
const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round);
|
|
300
733
|
const rolling = buildRollingSummary(request.loop_id, request.round, context);
|
|
734
|
+
const subGoals = manageSubGoals(request.loop_id, request.round, request.last_round_result, context);
|
|
301
735
|
const alignment = alignTask(request.task, request, context);
|
|
302
736
|
const health = checkLoopHealth(request.loop_id, { ...request, loop_objective: objective }, context);
|
|
303
737
|
const warnings = unique([
|
|
@@ -306,13 +740,33 @@ export function compileLoop(request, context) {
|
|
|
306
740
|
? `Loop health recommends ${health.escalation_recommended}.`
|
|
307
741
|
: "",
|
|
308
742
|
]);
|
|
743
|
+
// ── v2.5: Agent trust score ──────────────────────────────────────────
|
|
744
|
+
const flags = request.verification_flags ?? [];
|
|
745
|
+
const errors = flags.filter((f) => f.severity === "error").length;
|
|
746
|
+
const warns = flags.filter((f) => f.severity === "warn").length;
|
|
747
|
+
const trustScore = Math.max(0, Math.min(1, 1.0 - (errors * 0.15) - (warns * 0.03)));
|
|
748
|
+
// Trend — derive from vault entries (violation counts per round as proxy).
|
|
749
|
+
// No new persistence; reconstructed same as milestones/sub-goals.
|
|
750
|
+
const completedEntries = loopEntries(request.loop_id, context)
|
|
751
|
+
.filter((e) => entryRound(e) >= 1 && entryRound(e) < request.round);
|
|
752
|
+
const trend = [];
|
|
753
|
+
for (const entry of completedEntries.slice(-10)) {
|
|
754
|
+
const viols = Array.isArray(entry.constraint_violations)
|
|
755
|
+
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
756
|
+
: [];
|
|
757
|
+
// Simple proxy: no violations = perfect trust, each violation = -0.1
|
|
758
|
+
const score = Math.max(0, 1.0 - viols.length * 0.1);
|
|
759
|
+
trend.push(Number(score.toFixed(2)));
|
|
760
|
+
}
|
|
309
761
|
const response = makeLoopCompileResponse({
|
|
310
762
|
status: AgentStatus.OK,
|
|
311
763
|
recompile_level: decision.level,
|
|
312
764
|
diff_from_previous: decision.reasons.join(","),
|
|
313
765
|
lineage: [`${request.loop_id}:r${request.round}`],
|
|
314
|
-
constraints_active:
|
|
766
|
+
constraints_active: constraintLifecycle.active,
|
|
315
767
|
constraints_retired: constraints.retired,
|
|
768
|
+
constraints_inactive: constraintLifecycle.inactive,
|
|
769
|
+
constraint_metadata: constraintLifecycle.metadata,
|
|
316
770
|
loop_id: request.loop_id,
|
|
317
771
|
round: request.round,
|
|
318
772
|
goal_id: deriveGoalId(request.loop_id, request.task, request.goal_id),
|
|
@@ -321,7 +775,9 @@ export function compileLoop(request, context) {
|
|
|
321
775
|
loop_health: health,
|
|
322
776
|
task_alignment: alignment,
|
|
323
777
|
rolling_summary: rolling,
|
|
324
|
-
|
|
778
|
+
sub_goals: subGoals,
|
|
779
|
+
agent_trust_score: request.round > 1 ? Number(trustScore.toFixed(2)) : undefined,
|
|
780
|
+
agent_trust_trend: request.round > 1 ? trend : [],
|
|
325
781
|
suggested_next_task: request.last_round_result?.next_action?.trim() ||
|
|
326
782
|
request.last_round_result?.emerged_subtasks?.[0] || "",
|
|
327
783
|
plan_source: request.plan_source,
|
|
@@ -331,6 +787,13 @@ export function compileLoop(request, context) {
|
|
|
331
787
|
const statePath = `${policy.state_file.directory}/${request.loop_id}-state.md`;
|
|
332
788
|
const state = createCanonicalLoopState(request, response, statePath);
|
|
333
789
|
const markdown = renderCanonicalStateMarkdown(state);
|
|
790
|
+
// v2.4–v2.5: Adaptive L2 budget — scales with loop complexity
|
|
791
|
+
const adaptiveL2 = policy.prompt.l2_adaptive_enabled
|
|
792
|
+
? Math.min(policy.prompt.l2_max_chars
|
|
793
|
+
+ (request.round * policy.prompt.l2_adaptive_round_factor)
|
|
794
|
+
+ ((rolling?.milestones?.length ?? 0) * policy.prompt.l2_adaptive_milestone_factor)
|
|
795
|
+
+ (subGoals.length * policy.prompt.l2_adaptive_subgoal_factor), policy.prompt.l2_adaptive_max_chars)
|
|
796
|
+
: policy.prompt.l2_max_chars;
|
|
334
797
|
const artifact = assemblePromptArtifact({
|
|
335
798
|
state,
|
|
336
799
|
level: decision.level,
|
|
@@ -339,7 +802,7 @@ export function compileLoop(request, context) {
|
|
|
339
802
|
budgets: {
|
|
340
803
|
l0: policy.prompt.l0_max_chars,
|
|
341
804
|
l1: policy.prompt.l1_max_chars,
|
|
342
|
-
l2:
|
|
805
|
+
l2: adaptiveL2,
|
|
343
806
|
},
|
|
344
807
|
attempt: request.attempt,
|
|
345
808
|
selfEvaluationBlock: buildSelfEvalBlock(request.round),
|