loopforge 3.9.4 → 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 +114 -40
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +276 -182
- 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 +97 -156
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +223 -419
- 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 +26 -38
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +196 -319
- 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 +27 -897
- 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 +166 -27
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +372 -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 +39 -23
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +368 -565
- 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 +255 -81
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +1552 -539
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +42 -18
- 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 +956 -282
- 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 -92
- 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 +707 -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 +11 -39
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +12 -12
- 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 +20 -6
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +475 -479
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-coverage.d.ts +0 -6
- package/dist/prompt-coverage.d.ts.map +1 -1
- package/dist/prompt-coverage.js +4 -8
- package/dist/prompt-coverage.js.map +1 -1
- package/dist/prompt-freshness.d.ts +6 -17
- package/dist/prompt-freshness.d.ts.map +1 -1
- package/dist/prompt-freshness.js +27 -22
- package/dist/prompt-freshness.js.map +1 -1
- package/dist/prompt-policy.d.ts +14 -7
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +21 -5
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +843 -424
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +73 -102
- 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 +41 -89
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +250 -267
- 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 +51 -160
- 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 +837 -38
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +37 -61
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +413 -392
- 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 +29 -21
- package/dist/submission-guidance.d.ts.map +1 -1
- package/dist/submission-guidance.js +308 -260
- 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 +145 -51
- package/dist/submission-receipt.js.map +1 -1
- package/dist/token-utils.d.ts +28 -7
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +27 -11
- 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.map +1 -1
- package/dist/verification-actions.js +10 -11
- package/dist/verification-actions.js.map +1 -1
- package/dist/verification-gate.d.ts +23 -47
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +202 -423
- 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 +4 -18
- package/package.json +68 -68
- 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 -389
package/dist/loop-compiler.js
CHANGED
|
@@ -5,219 +5,186 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { deriveConfiguredCapability, getPolicy } from "./policy.js";
|
|
8
|
-
import { AgentStatus, makeLoopCompileResponse, makeLoopObjective, makeConstraintMeta,
|
|
8
|
+
import { AgentStatus, makeLoopCompileResponse, makeLoopObjective, makeConstraintMeta, } from "./protocol.js";
|
|
9
9
|
import { createCanonicalLoopState, renderCanonicalStateMarkdown, } from "./canonical-state.js";
|
|
10
|
-
import {
|
|
10
|
+
import { pendingBacktrackRecord, roundResultFromCommittedRound, } from "./committed-round.js";
|
|
11
|
+
import { buildCommittedHistory, historyWindow, operatorAcceptanceKey, } from "./history.js";
|
|
12
|
+
import { LegacyHistoryError, OperatorAcceptanceConflictError } from "./errors.js";
|
|
13
|
+
import { deriveProgressWindow, } from "./progress.js";
|
|
14
|
+
import { deriveCriterionStatuses as deriveCriterionStatusesFromViews } from "./criterion-status.js";
|
|
11
15
|
import { derivePromptFreshness, UNKNOWN_PROMPT_FRESHNESS, } from "./prompt-freshness.js";
|
|
12
16
|
import { NO_IN_FLIGHT_ROUND, deriveRoundFacts } from "./round-facts.js";
|
|
13
17
|
import { recoveryBriefLines } from "./enforcement-gate.js";
|
|
14
18
|
import { deriveSubGoals, duplicateEmergedDeclarations } from "./subgoal-state.js";
|
|
15
|
-
import {
|
|
19
|
+
import { deriveConstraintProvenance, resolveConstraintRetractions, } from "./constraint-lifecycle.js";
|
|
16
20
|
import { assemblePromptArtifact } from "./prompt-assembler.js";
|
|
21
|
+
import { derivePromptContextBoundary } from "./context-boundary.js";
|
|
22
|
+
import { deriveWorkingMemory } from "./working-memory.js";
|
|
17
23
|
import { renderSelfEvalBlock, } from "./submission-guidance.js";
|
|
18
24
|
import { decidePromptLevel, } from "./prompt-policy.js";
|
|
19
|
-
import { criterionRefMatches,
|
|
25
|
+
import { criterionRefMatches, deriveConstraintId, deriveCriterionId, normalizeText, unique, uniqueNormalized, } from "./token-utils.js";
|
|
20
26
|
function contextEntries(context) {
|
|
21
27
|
if (!context || !Array.isArray(context.results))
|
|
22
28
|
return [];
|
|
23
29
|
return context.results.filter((value) => value !== null && typeof value === "object" && !Array.isArray(value));
|
|
24
30
|
}
|
|
25
|
-
function loopEntries(loopId, context) {
|
|
26
|
-
return contextEntries(context)
|
|
27
|
-
.filter((entry) => entry.loop_id === loopId || entryLineage(entry).loop_id === loopId)
|
|
28
|
-
.sort((a, b) => entryRound(a) - entryRound(b));
|
|
29
|
-
}
|
|
30
|
-
/** v3.3.1: The canonical entry for a round — its compile-time lineage entry
|
|
31
|
-
* (task_type "loop_lineage"; engine hydration merges the committed
|
|
32
|
-
* decision into it, so one round = one merged lineage entry in the
|
|
33
|
-
* compiler's view). Event/journal entries (delegation journals, gate
|
|
34
|
-
* records) share lineage.round with the round but carry none of its
|
|
35
|
-
* compile-time fields; round-level consumers that matched on the round
|
|
36
|
-
* number alone could hit them first (they sort after the lineage entry of
|
|
37
|
-
* the same round), producing an empty goal_id / task / constraints_active
|
|
38
|
-
* and forcing spurious L2 recovery. A direct :feedback entry is accepted
|
|
39
|
-
* when the caller supplies a raw committed view instead of hydration. */
|
|
40
|
-
function roundCanonicalEntry(entries, round) {
|
|
41
|
-
const candidates = entries.filter((entry) => entryRound(entry) === round);
|
|
42
|
-
return (candidates.find((entry) => entry.task_type === "loop_lineage") ??
|
|
43
|
-
candidates.find((entry) => String(entry.task_id ?? "").endsWith(":feedback")) ??
|
|
44
|
-
// Untyped compiler inputs are treated as round views, not events. Known event
|
|
45
|
-
// types (delegation journals, gate records) are excluded explicitly so
|
|
46
|
-
// they can never shadow a round again.
|
|
47
|
-
candidates.find((entry) => {
|
|
48
|
-
const type = String(entry.task_type ?? "");
|
|
49
|
-
return type === "" || (!type.startsWith("delegation") && !type.endsWith("journal"));
|
|
50
|
-
}) ??
|
|
51
|
-
null);
|
|
52
|
-
}
|
|
53
31
|
export function computeGoalTextHash(text) {
|
|
54
32
|
const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
|
|
55
33
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
|
|
56
34
|
}
|
|
57
|
-
|
|
58
|
-
|
|
35
|
+
/** v4.0.6: goal identity derives from the WORKING objective (the latest
|
|
36
|
+
* committed objective_refinement, else Operator Acceptance). There is no
|
|
37
|
+
* caller-supplied goal_id override. */
|
|
38
|
+
export function deriveGoalId(loopId, task) {
|
|
39
|
+
const slug = task.trim().toLowerCase()
|
|
59
40
|
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-")
|
|
60
41
|
.replace(/^-|-$/g, "")
|
|
61
|
-
.slice(0, 48)
|
|
42
|
+
.slice(0, 48);
|
|
43
|
+
// Keep the readable prefix while making the identity depend on the full
|
|
44
|
+
// normalized objective; shared prefixes must not collapse distinct goals.
|
|
45
|
+
return `${loopId}:${slug || "goal"}:${computeGoalTextHash(task)}`;
|
|
62
46
|
}
|
|
63
47
|
export function getPreviousRound(loopId, round, context) {
|
|
64
|
-
const
|
|
65
|
-
|
|
48
|
+
const history = buildCommittedHistory(contextEntries(context));
|
|
49
|
+
return previousRoundFromWindow(historyWindow(history, Number.POSITIVE_INFINITY), round);
|
|
50
|
+
}
|
|
51
|
+
function previousRoundFromWindow(window, round) {
|
|
52
|
+
const view = window.find((candidate) => candidate.round === round);
|
|
53
|
+
if (!view)
|
|
66
54
|
return null;
|
|
67
|
-
const data = entryLineage(entry);
|
|
68
55
|
return {
|
|
69
56
|
round,
|
|
70
|
-
goal_id:
|
|
71
|
-
goal_text_hash: typeof data.goal_text_hash === "string" ? data.goal_text_hash : "",
|
|
72
|
-
success: typeof entry.success === "boolean"
|
|
73
|
-
? entry.success
|
|
74
|
-
: typeof data.success === "boolean" ? data.success : false,
|
|
75
|
-
task: typeof data.task === "string"
|
|
76
|
-
? data.task
|
|
77
|
-
: typeof entry.task === "string" ? entry.task : "",
|
|
78
|
-
constraints_active: Array.isArray(data.constraints_active)
|
|
79
|
-
? data.constraints_active.filter((value) => typeof value === "string")
|
|
80
|
-
: [],
|
|
81
|
-
output_summary: typeof entry.output_summary === "string"
|
|
82
|
-
? entry.output_summary
|
|
83
|
-
: typeof data.output_summary === "string" ? data.output_summary : "",
|
|
57
|
+
goal_id: view.compiled?.goalId ?? "",
|
|
84
58
|
};
|
|
85
59
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
*
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
? value.hard_constraints.filter((item) => typeof item === "string")
|
|
113
|
-
: [],
|
|
114
|
-
created_at_round: typeof value.created_at_round === "number" ? value.created_at_round : 1,
|
|
115
|
-
loop_id: loopId,
|
|
116
|
-
version: typeof value.version === "number" ? value.version : 1,
|
|
117
|
-
refinement_history: Array.isArray(value.refinement_history)
|
|
118
|
-
? value.refinement_history.filter((item) => typeof item === "string")
|
|
119
|
-
: [],
|
|
120
|
-
});
|
|
60
|
+
/** v4.0.6: THE working objective.
|
|
61
|
+
*
|
|
62
|
+
* - The immutable operator surface is the committed `OperatorAcceptance`.
|
|
63
|
+
* Acceptance is byte-identical across copies (enforced by the history
|
|
64
|
+
* loader), so it does not need a round or branch window.
|
|
65
|
+
* - The objective TEXT is the latest committed `objective_refinement` on the
|
|
66
|
+
* CURRENT BRANCH, else the acceptance task. A refinement from a branch
|
|
67
|
+
* that was rolled back never applies.
|
|
68
|
+
* - Success criteria and hard constraints ALWAYS come from the acceptance; a
|
|
69
|
+
* refinement can never rewrite them.
|
|
70
|
+
*
|
|
71
|
+
* Read-only callers that only need criteria/hard constraints read those
|
|
72
|
+
* fields; they must not treat `objective` as the operator acceptance text. */
|
|
73
|
+
export function latestObjective(loopId, context) {
|
|
74
|
+
const history = buildCommittedHistory(contextEntries(context));
|
|
75
|
+
const acceptanceView = history.rounds.find((view) => view.operatorAcceptance) ??
|
|
76
|
+
history.views.find((view) => view.operatorAcceptance);
|
|
77
|
+
if (!acceptanceView?.operatorAcceptance)
|
|
78
|
+
return null;
|
|
79
|
+
const acceptance = acceptanceView.operatorAcceptance;
|
|
80
|
+
let objective = acceptance.task;
|
|
81
|
+
for (const view of history.rounds) {
|
|
82
|
+
const refinement = view.objectiveRefinement;
|
|
83
|
+
if (typeof refinement === "string" && refinement.trim().length > 0) {
|
|
84
|
+
objective = refinement;
|
|
85
|
+
}
|
|
121
86
|
}
|
|
122
|
-
return
|
|
87
|
+
return makeLoopObjective({
|
|
88
|
+
objective,
|
|
89
|
+
success_criteria: [...acceptance.success_criteria],
|
|
90
|
+
hard_constraints: [...acceptance.hard_constraints],
|
|
91
|
+
created_at_round: 1,
|
|
92
|
+
loop_id: loopId,
|
|
93
|
+
});
|
|
123
94
|
}
|
|
124
|
-
function evolveObjective(request, context,
|
|
125
|
-
|
|
126
|
-
|
|
95
|
+
function evolveObjective(request, context, operatorAcceptance) {
|
|
96
|
+
// v3.9.7: committed acceptance is authoritative once it exists. A transient
|
|
97
|
+
// request may bootstrap the objective for round 1 or a library caller with
|
|
98
|
+
// no committed objective; it may not rewrite operator acceptance after a
|
|
99
|
+
// committed objective exists.
|
|
100
|
+
const committed = latestObjective(request.loop_id, context);
|
|
101
|
+
const allowRequestObjective = request.loop_objective !== null &&
|
|
102
|
+
(request.round === 1 || committed === null);
|
|
103
|
+
// v4.0.6: the accepted objective's TEXT is the working objective
|
|
104
|
+
// (committed refinement or acceptance task); criteria/hard constraints come
|
|
105
|
+
// from the immutable acceptance. The refinement never rewrites acceptance.
|
|
106
|
+
const accepted = operatorAcceptance
|
|
107
|
+
? makeLoopObjective({
|
|
108
|
+
objective: committed?.objective || operatorAcceptance.task,
|
|
109
|
+
success_criteria: [...operatorAcceptance.success_criteria],
|
|
110
|
+
hard_constraints: [...operatorAcceptance.hard_constraints],
|
|
111
|
+
created_at_round: 1,
|
|
112
|
+
loop_id: request.loop_id,
|
|
113
|
+
})
|
|
114
|
+
: null;
|
|
115
|
+
const current = accepted ??
|
|
116
|
+
(allowRequestObjective ? request.loop_objective : null) ??
|
|
117
|
+
committed ??
|
|
127
118
|
makeLoopObjective({
|
|
128
119
|
objective: request.task,
|
|
129
120
|
success_criteria: ["Task goal achieved with verifiable evidence"],
|
|
130
|
-
hard_constraints: unique(request.constraints_from_plan),
|
|
121
|
+
hard_constraints: unique(request.round === 1 || committed === null ? request.constraints_from_plan : []),
|
|
131
122
|
created_at_round: 1,
|
|
132
123
|
loop_id: request.loop_id,
|
|
133
124
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
let objective = current.objective || request.task;
|
|
137
|
-
let version = current.version ?? 1;
|
|
138
|
-
if (last?.objective_refinement?.trim()) {
|
|
139
|
-
const refinement = last.objective_refinement.trim();
|
|
140
|
-
// M5: fold each refinement exactly once per persisted objective state.
|
|
141
|
-
// A retry recompiles the same round against the same committed
|
|
142
|
-
// previous-round projection; without this guard every retry attempt
|
|
143
|
-
// re-appends the refinement and inflates the version — the objective text
|
|
144
|
-
// and the state file grew "Refinement: X" once per attempt.
|
|
145
|
-
if (!history.includes(refinement)) {
|
|
146
|
-
history.push(refinement);
|
|
147
|
-
objective = `${objective}\nRefinement: ${refinement}`;
|
|
148
|
-
version++;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const revisions = new Map((last?.revised_success_criteria ?? []).map((item) => [item.old, item.new]));
|
|
125
|
+
// The operator objective is the committed acceptance; no Agent report
|
|
126
|
+
// rewrites it.
|
|
152
127
|
return makeLoopObjective({
|
|
153
128
|
...current,
|
|
154
|
-
objective,
|
|
155
|
-
|
|
156
|
-
refinement_history: history.slice(-getPolicy().evolution.max_objective_versions),
|
|
157
|
-
// v3.9.4: one normalized-exact text, one cr-* identity. `unique` was
|
|
158
|
-
// trim-only, so case/whitespace variants survived as two entries that
|
|
159
|
-
// derived to the SAME criterion id.
|
|
160
|
-
success_criteria: uniqueNormalized(current.success_criteria
|
|
161
|
-
.filter((item) => typeof item === "string")
|
|
162
|
-
.map((item) => revisions.get(item) ?? item)),
|
|
129
|
+
objective: current.objective || request.task,
|
|
130
|
+
success_criteria: uniqueNormalized(current.success_criteria),
|
|
163
131
|
hard_constraints: uniqueNormalized(current.hard_constraints),
|
|
164
132
|
loop_id: request.loop_id,
|
|
165
133
|
});
|
|
166
134
|
}
|
|
167
|
-
function evolveConstraints(request, objective, previous, windowRounds) {
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
// v3.9.4: normalized-exact dedupe. The trim-only `unique` let case/space
|
|
201
|
-
// variants coexist while deriving the SAME c-/cr- id. The silent
|
|
202
|
-
// max_active_constraints slice is gone: every merged active constraint is
|
|
203
|
-
// normative, and prompt budget pressure is reported as protectedOverflow.
|
|
204
|
-
const active = uniqueNormalized([
|
|
205
|
-
...(previous.view?.activeConstraints ?? []),
|
|
206
|
-
...request.constraints_from_plan,
|
|
135
|
+
function evolveConstraints(request, objective, previous, windowRounds, roundOnePlan, preservedDiscoveries, preservedRound) {
|
|
136
|
+
// The active set at the START of the round that produced the retraction:
|
|
137
|
+
// the previous committed compile's stamped `constraints_active`.
|
|
138
|
+
const carried = uniqueNormalized(previous.view?.activeConstraints ?? []);
|
|
139
|
+
const activeTargets = carried.map((text) => ({
|
|
140
|
+
text,
|
|
141
|
+
provenance: deriveConstraintProvenance({
|
|
142
|
+
text,
|
|
143
|
+
objective,
|
|
144
|
+
windowRounds,
|
|
145
|
+
roundOnePlan,
|
|
146
|
+
preservedDiscoveries,
|
|
147
|
+
preservedRound,
|
|
148
|
+
}),
|
|
149
|
+
}));
|
|
150
|
+
const previouslyRetired = unique(windowRounds.flatMap((round) => round.retractedConstraints ?? []));
|
|
151
|
+
const retraction = resolveConstraintRetractions({
|
|
152
|
+
targets: previous.result?.retracted_constraints ?? [],
|
|
153
|
+
active: activeTargets,
|
|
154
|
+
// The compiler does not see this submission's new discoveries; the
|
|
155
|
+
// boundary resolves the same target set and reports same-round cases.
|
|
156
|
+
newDiscoveries: [],
|
|
157
|
+
previouslyRetired,
|
|
158
|
+
});
|
|
159
|
+
const effectiveKeys = new Set(retraction.effective.map(normalizeText));
|
|
160
|
+
// Only round 1 accepts a plan input. After round 1 the active set is carried
|
|
161
|
+
// from committed compile facts; a late request value is not a fact.
|
|
162
|
+
const base = uniqueNormalized([
|
|
163
|
+
...carried,
|
|
164
|
+
...roundOnePlan,
|
|
207
165
|
...objective.hard_constraints,
|
|
208
166
|
...objective.success_criteria,
|
|
209
|
-
...
|
|
210
|
-
]).filter((
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
167
|
+
...preservedDiscoveries,
|
|
168
|
+
]).filter((text) => !effectiveKeys.has(normalizeText(text)));
|
|
169
|
+
// Only the previous round's NEW discoveries are added after retraction:
|
|
170
|
+
// a new declaration in the retraction round survives the retraction.
|
|
171
|
+
const allDiscovered = unique(previous.result?.discovered_constraints ?? []);
|
|
172
|
+
const discovered = allDiscovered
|
|
173
|
+
.slice(0, getPolicy().evolution.max_discovered_constraints_per_round);
|
|
174
|
+
const discoveryDropped = allDiscovered.length - discovered.length;
|
|
175
|
+
const active = uniqueNormalized([...base, ...discovered]);
|
|
176
|
+
const provenance = new Map();
|
|
177
|
+
for (const text of active) {
|
|
178
|
+
provenance.set(text, deriveConstraintProvenance({
|
|
179
|
+
text,
|
|
180
|
+
objective,
|
|
181
|
+
windowRounds,
|
|
182
|
+
roundOnePlan,
|
|
183
|
+
preservedDiscoveries,
|
|
184
|
+
preservedRound,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
return { active, retired: retraction.effective, provenance, discoveryDropped };
|
|
221
188
|
}
|
|
222
189
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
223
190
|
// Hierarchical Summary (v2.1)
|
|
@@ -272,18 +239,27 @@ export function deriveRecurringFlags(committedRounds) {
|
|
|
272
239
|
* — the dashboard then shows no machine row. Display-only companion to the
|
|
273
240
|
* R4/R5 exculpatory signal. */
|
|
274
241
|
function deriveMachineStatus(rounds, currentRound) {
|
|
275
|
-
|
|
276
|
-
|
|
242
|
+
// v3.9.9: the dashboard shows the SAME fact the stall rule consumes.
|
|
243
|
+
const window = deriveProgressWindow(rounds, currentRound, getPolicy().engine.stall_lookback_rounds);
|
|
244
|
+
if (window.incomplete || window.state === "not_applicable")
|
|
277
245
|
return undefined;
|
|
278
|
-
const
|
|
279
|
-
return {
|
|
246
|
+
const changedRounds = window.roundStates.filter((entry) => entry.state === "changed").length;
|
|
247
|
+
return {
|
|
248
|
+
windowRounds: window.lookback,
|
|
249
|
+
machineProgress: window.state,
|
|
250
|
+
changedRounds,
|
|
251
|
+
providers: window.providers,
|
|
252
|
+
};
|
|
280
253
|
}
|
|
281
254
|
/** v3.2: Derive per-criterion status — the "goal → criteria → evidence"
|
|
282
|
-
* vertical view. Each objective criterion gets
|
|
283
|
-
* (from per-round
|
|
284
|
-
* the round it was first reported met
|
|
285
|
-
*
|
|
286
|
-
*
|
|
255
|
+
* vertical view. Each objective criterion gets claimed/remaining/unknown
|
|
256
|
+
* (from the `cr-` entries of per-round execution_report.claims, ID-first or
|
|
257
|
+
* normalized-exact matching) and the round it was first reported met. Zero
|
|
258
|
+
* persistence — re-derived from committed rounds every compile.
|
|
259
|
+
*
|
|
260
|
+
* v3.9.6: the derivation lives in `criterion-status.ts` so the submission
|
|
261
|
+
* boundary and guidance can read the same rows without importing this module
|
|
262
|
+
* (which would close the `loop-compiler → submission-guidance` cycle). */
|
|
287
263
|
export function deriveCriterionStatuses(loopId, context, objective, currentRound, lastRoundResult,
|
|
288
264
|
/** v3.8.1: the criterion machine facts from the shared bundle. A criterion
|
|
289
265
|
* is `verified` / `contradicted` / `insufficient` only through an item that
|
|
@@ -291,65 +267,13 @@ export function deriveCriterionStatuses(loopId, context, objective, currentRound
|
|
|
291
267
|
* is derived over the WHOLE committed history, so it survives its contract
|
|
292
268
|
* closing (see `deriveCriterionFacts`). */
|
|
293
269
|
criterionFacts = []) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const lastRemaining = claimedRemainingCriteria(lastEv);
|
|
302
|
-
const lastEntry = entries[entries.length - 1] ?? null;
|
|
303
|
-
const lastEntryRemaining = lastEntry ? entryCriteriaRemaining(lastEntry) : [];
|
|
304
|
-
// v3.8.1: criterion → machine status + explicit sub-goal links, from the
|
|
305
|
-
// shared `RoundFacts` bundle (whole committed history). It used to be
|
|
306
|
-
// re-derived here from the ACTIVE contract, which made both maps empty the
|
|
307
|
-
// moment the contract closed — the criterion's machine fact, and the
|
|
308
|
-
// sub-goal links carried on the same item, disappeared while the sub-goal
|
|
309
|
-
// side of the very same fact survived. One derivation, one window.
|
|
310
|
-
const factsByCriterion = new Map(criterionFacts.map((fact) => [fact.criterion_id, fact]));
|
|
311
|
-
// v3.9.4: one row per derived id. The objective is authored text; a case or
|
|
312
|
-
// whitespace variant derives the same cr-* and must not produce a second
|
|
313
|
-
// row (nor a second contract reference space).
|
|
314
|
-
const seenIds = new Set();
|
|
315
|
-
const statuses = [];
|
|
316
|
-
for (const text of objective.success_criteria) {
|
|
317
|
-
const id = deriveCriterionId(text);
|
|
318
|
-
if (seenIds.has(id))
|
|
319
|
-
continue;
|
|
320
|
-
seenIds.add(id);
|
|
321
|
-
let metAtRound = null;
|
|
322
|
-
for (const entry of entries) {
|
|
323
|
-
const rnd = entryRound(entry);
|
|
324
|
-
if (entryCriteriaMet(entry).some((met) => criteriaMatch(met, text))) {
|
|
325
|
-
metAtRound = rnd;
|
|
326
|
-
break; // first report wins — chronological scan
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
if (metAtRound === null && lastMet.some((met) => criteriaMatch(met, text))) {
|
|
330
|
-
metAtRound = currentRound - 1;
|
|
331
|
-
}
|
|
332
|
-
// v3.8: a claim reaches `claimed` at most. Only a machine fact about a
|
|
333
|
-
// contract item that references this criterion can go further.
|
|
334
|
-
const claimedStatus = metAtRound !== null
|
|
335
|
-
? "claimed"
|
|
336
|
-
: lastRemaining.some((remaining) => criteriaMatch(remaining, text)) ||
|
|
337
|
-
lastEntryRemaining.some((remaining) => criteriaMatch(remaining, text))
|
|
338
|
-
? "remaining"
|
|
339
|
-
: "unknown";
|
|
340
|
-
const fact = factsByCriterion.get(id);
|
|
341
|
-
const status = fact?.status ?? claimedStatus;
|
|
342
|
-
statuses.push({
|
|
343
|
-
id,
|
|
344
|
-
text,
|
|
345
|
-
status,
|
|
346
|
-
...(metAtRound !== null ? { met_at_round: metAtRound } : {}),
|
|
347
|
-
// v3.8.1: explicit refs only — empty when no contract item links this
|
|
348
|
-
// criterion to a sub-goal.
|
|
349
|
-
related_subgoal_ids: fact?.related_subgoal_ids ?? [],
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
return statuses;
|
|
270
|
+
const history = buildCommittedHistory(contextEntries(context));
|
|
271
|
+
return deriveCriterionStatusesFromViews({
|
|
272
|
+
rounds: historyWindow(history, currentRound),
|
|
273
|
+
currentRound,
|
|
274
|
+
objective,
|
|
275
|
+
criterionFacts,
|
|
276
|
+
});
|
|
353
277
|
}
|
|
354
278
|
/** v3.8: Rank item statuses so the strongest machine fact wins when several
|
|
355
279
|
* items reference the same criterion. */
|
|
@@ -361,189 +285,16 @@ export function criteriaMatch(a, b) {
|
|
|
361
285
|
// v3.9.2: ONE identity rule, shared with the prompt and submission boundary.
|
|
362
286
|
return criterionRefMatches(a, b);
|
|
363
287
|
}
|
|
364
|
-
/** Detect newly met criteria by comparing the current entry's
|
|
365
|
-
* met criterion_claims against the previous entry's.
|
|
366
|
-
* v2.11/v3.8.1: id-first (cr-XXXXXXXX), then normalized-exact text —
|
|
367
|
-
* no similarity.
|
|
368
|
-
* Returns empty array when there is no previous entry — the first
|
|
369
|
-
* entry's criteria are the baseline, not a "new" event. */
|
|
370
|
-
function detectNewCriteria(current, previous) {
|
|
371
|
-
const currMet = entryCriteriaMet(current);
|
|
372
|
-
if (currMet.length === 0)
|
|
373
|
-
return [];
|
|
374
|
-
if (!previous) {
|
|
375
|
-
// First round with criteria is the baseline — not a milestone trigger.
|
|
376
|
-
return [];
|
|
377
|
-
}
|
|
378
|
-
const prevMet = entryCriteriaMet(previous);
|
|
379
|
-
if (prevMet.length === 0)
|
|
380
|
-
return currMet;
|
|
381
|
-
return currMet.filter((curr) => prevMet.every((prev) => !criteriaMatch(curr, prev)));
|
|
382
|
-
}
|
|
383
|
-
/** Build a single MilestoneSummary from a range of completed round entries. */
|
|
384
|
-
function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, kind) {
|
|
385
|
-
const last = phaseEntries.at(-1);
|
|
386
|
-
const lastView = last ? decodeRound(last) : null;
|
|
387
|
-
const outcomeRaw = lastView
|
|
388
|
-
? (lastView.outputSummary ?? "")
|
|
389
|
-
: last && typeof last.output_summary === "string" ? last.output_summary : "";
|
|
390
|
-
const outcomeMax = getPolicy().checkpoint.outcome_max_chars;
|
|
391
|
-
const outcome = outcomeRaw.length > outcomeMax
|
|
392
|
-
? outcomeRaw.slice(0, outcomeMax) + "…"
|
|
393
|
-
: outcomeRaw;
|
|
394
|
-
const carried = last ? entryActiveConstraints(last) : [];
|
|
395
|
-
const resolved = unique(phaseEntries.flatMap((e) => entryRetractedConstraints(e)));
|
|
396
|
-
return makeMilestoneSummary({
|
|
397
|
-
label,
|
|
398
|
-
round_range: { start: startRound, end: endRound },
|
|
399
|
-
outcome,
|
|
400
|
-
carried_constraints: carried,
|
|
401
|
-
resolved_constraints: resolved,
|
|
402
|
-
kind,
|
|
403
|
-
generated_at_round: endRound,
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
/** v3.8.1: the milestone history bound. Applied identically at every prompt
|
|
407
|
-
* level and in the state file, so the milestone set is a function of
|
|
408
|
-
* committed facts rather than of which level happened to compile. (The
|
|
409
|
-
* v3.0.1 sampler chose which milestones to keep from an UNBOUNDED list and
|
|
410
|
-
* did nothing at L2 — so the state file’s phase history depended on the
|
|
411
|
-
* prompt level and had no bound of its own.) */
|
|
412
|
-
const MILESTONE_HISTORY_CAP = 50;
|
|
413
|
-
export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0) {
|
|
414
|
-
const policy = getPolicy().summary;
|
|
415
|
-
const allEntries = loopEntries(loopId, context);
|
|
416
|
-
// ── Phase 1: Recent Window (preserves existing behavior) ──
|
|
417
|
-
const windowEntries = allEntries
|
|
418
|
-
.filter((entry) => {
|
|
419
|
-
const round = entryRound(entry);
|
|
420
|
-
return round >= sinceRound && round < currentRound;
|
|
421
|
-
})
|
|
422
|
-
.slice(-policy.window);
|
|
423
|
-
const outcomes = [];
|
|
424
|
-
for (const entry of windowEntries) {
|
|
425
|
-
const view = decodeRound(entry);
|
|
426
|
-
const data = entryLineage(entry);
|
|
427
|
-
const round = entryRound(entry);
|
|
428
|
-
const success = view ? view.success : entry.success ?? data.success;
|
|
429
|
-
const summary = view
|
|
430
|
-
? view.outputSummary ?? ""
|
|
431
|
-
: typeof entry.output_summary === "string"
|
|
432
|
-
? entry.output_summary
|
|
433
|
-
: typeof data.output_summary === "string" ? data.output_summary : "";
|
|
434
|
-
// v2.12: Declared outcome wins the label; the default wording stays
|
|
435
|
-
// "accepted" for zero regression on existing loop output.
|
|
436
|
-
const declared = view ? view.outcome : entry.outcome;
|
|
437
|
-
const outcomeLabel = declared === "success" || declared === "partial" ||
|
|
438
|
-
declared === "failed" || declared === "blocked"
|
|
439
|
-
? declared
|
|
440
|
-
: success === false ? "failed" : "accepted";
|
|
441
|
-
if (summary)
|
|
442
|
-
outcomes.push(`[R${round}] ${outcomeLabel}: ${summary}`);
|
|
443
|
-
}
|
|
444
|
-
// ── Phase 2: Milestone Accumulation ──
|
|
445
|
-
// Scan all completed entries in round order. For each entry, check three
|
|
446
|
-
// trigger signals in priority order: agent_declared > criteria_milestone > auto.
|
|
447
|
-
const completedEntries = allEntries.filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
|
|
448
|
-
const milestones = [];
|
|
449
|
-
let lastMilestoneRound = 0;
|
|
450
|
-
for (let i = 0; i < completedEntries.length; i++) {
|
|
451
|
-
const entry = completedEntries[i];
|
|
452
|
-
const rnd = entryRound(entry);
|
|
453
|
-
const lin = entryLineage(entry);
|
|
454
|
-
// Signal 1: Agent-declared checkpoint
|
|
455
|
-
const isAgentCheckpoint = lin.compression_checkpoint === true;
|
|
456
|
-
// Signal 2: New criteria met (compare with previous vault entry)
|
|
457
|
-
const prev = i > 0 ? completedEntries[i - 1] : null;
|
|
458
|
-
const newCriteria = detectNewCriteria(entry, prev);
|
|
459
|
-
if (isAgentCheckpoint || newCriteria.length > 0) {
|
|
460
|
-
const startRound = lastMilestoneRound + 1;
|
|
461
|
-
const phaseEntries = completedEntries.filter((e) => {
|
|
462
|
-
const er = entryRound(e);
|
|
463
|
-
return er >= startRound && er <= rnd;
|
|
464
|
-
});
|
|
465
|
-
const kind = isAgentCheckpoint
|
|
466
|
-
? "agent_declared"
|
|
467
|
-
: "criteria_milestone";
|
|
468
|
-
const label = isAgentCheckpoint
|
|
469
|
-
? (typeof lin.checkpoint_label === "string" && lin.checkpoint_label.trim()
|
|
470
|
-
? lin.checkpoint_label.trim()
|
|
471
|
-
: `Round ${rnd}`)
|
|
472
|
-
: `Completed: ${newCriteria.join(", ")}`;
|
|
473
|
-
milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, rnd, label, kind));
|
|
474
|
-
lastMilestoneRound = rnd;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
// Signal 3: Safety net — auto milestone when gap exceeds milestone_interval.
|
|
478
|
-
// v3.3.1: the policy contract (milestone_interval: "only fires when no
|
|
479
|
-
// agent_declared or criteria_milestone has been created for this many
|
|
480
|
-
// rounds") includes loops that NEVER created a milestone — but the guard
|
|
481
|
-
// `lastMilestoneRound > 0` made the gap start from the (nonexistent) first
|
|
482
|
-
// milestone, so a checkpoint-less loop could run forever with an empty
|
|
483
|
-
// Phase History and no memory skeleton. With lastMilestoneRound == 0 the
|
|
484
|
-
// gap is measured from round 1 and the safety net fires from the very
|
|
485
|
-
// first interval.
|
|
486
|
-
const gapSinceLast = (currentRound - 1) - lastMilestoneRound;
|
|
487
|
-
if (gapSinceLast >= policy.milestone_interval &&
|
|
488
|
-
completedEntries.length > 0) {
|
|
489
|
-
const startRound = lastMilestoneRound + 1;
|
|
490
|
-
const endRound = currentRound - 1;
|
|
491
|
-
const phaseEntries = completedEntries.filter((e) => {
|
|
492
|
-
const er = entryRound(e);
|
|
493
|
-
return er >= startRound && er <= endRound;
|
|
494
|
-
});
|
|
495
|
-
if (phaseEntries.length > 0) {
|
|
496
|
-
milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, endRound, `Rounds ${startRound}–${endRound}`, "auto"));
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
// v3.8.1: one bound, applied identically at every prompt level. The v3.0.1
|
|
500
|
-
// sampler kept head anchors + a newest tail + an even middle sample, and
|
|
501
|
-
// deliberately did nothing at L2 — so the milestone set written to the state
|
|
502
|
-
// file depended on which level happened to compile, not on committed facts.
|
|
503
|
-
// It also did not bound growth at all (it only chose WHICH milestones to
|
|
504
|
-
// keep from an unbounded list), which is why the cap has to live here.
|
|
505
|
-
const cappedMilestones = milestones.slice(-MILESTONE_HISTORY_CAP);
|
|
506
|
-
if (windowEntries.length === 0 && cappedMilestones.length === 0)
|
|
507
|
-
return null;
|
|
508
|
-
return makeRollingSummary({
|
|
509
|
-
key_outcomes: unique(outcomes),
|
|
510
|
-
rounds_sampled: windowEntries.length,
|
|
511
|
-
generated_at_round: currentRound,
|
|
512
|
-
failed_patterns: [],
|
|
513
|
-
milestones: cappedMilestones,
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
288
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
517
289
|
// Sub-Goal Structured Tracking (v2.2)
|
|
518
290
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
// v3.8.1: `deriveCriterionId` / `isCriterionId` moved to token-utils.ts, where
|
|
525
|
-
// the other stable-id helpers live — the criterion fact derivation in
|
|
526
|
-
// round-facts.ts needs them, and defining them here would make
|
|
527
|
-
// round-facts ⇄ loop-compiler an import cycle. Re-exported so this module's
|
|
528
|
-
// public surface ("./compiler") is unchanged.
|
|
529
|
-
export { deriveCriterionId, isCriterionId } from "./token-utils.js";
|
|
291
|
+
// v3.8.1/v3.9.7: the stable-id helpers live in token-utils.ts, where the
|
|
292
|
+
// criterion/constraint fact derivations can use them without an import
|
|
293
|
+
// cycle. Re-exported so this module's public surface is unchanged.
|
|
294
|
+
export { deriveConstraintId, deriveCriterionId, isCriterionId, } from "./token-utils.js";
|
|
530
295
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
531
296
|
// Time-Aware Constraint Management (v2.3)
|
|
532
297
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
533
|
-
/** Determine the source of a constraint by matching against known sources. */
|
|
534
|
-
function constraintSource(text, objective, constraintsFromPlan) {
|
|
535
|
-
const norm = normalizeText(text);
|
|
536
|
-
// v3.9.4: criteria identity wins. A text that is both a criterion and a
|
|
537
|
-
// hard/plan constraint keeps its cr-* id everywhere; the prompt partition
|
|
538
|
-
// renders it once, in the criteria section.
|
|
539
|
-
if (objective.success_criteria.some((c) => normalizeText(c) === norm))
|
|
540
|
-
return "criteria";
|
|
541
|
-
if (objective.hard_constraints.some((c) => normalizeText(c) === norm))
|
|
542
|
-
return "hard";
|
|
543
|
-
if (constraintsFromPlan.some((c) => normalizeText(c) === norm))
|
|
544
|
-
return "plan";
|
|
545
|
-
return "discovered";
|
|
546
|
-
}
|
|
547
298
|
/** True when a reported violation text matches a constraint text.
|
|
548
299
|
* v2.11: ID-first — an exact `c-XXXXXXXX` match against the derived id.
|
|
549
300
|
* v3.8.1: the Jaccard fallback is gone; the remaining arm requires the
|
|
@@ -557,22 +308,13 @@ function matchesConstraintText(text, violation) {
|
|
|
557
308
|
}
|
|
558
309
|
return normalizeText(text) === normalizeText(violation);
|
|
559
310
|
}
|
|
560
|
-
function findLastViolatedRound(text,
|
|
311
|
+
function findLastViolatedRound(text, rounds) {
|
|
561
312
|
let latest = 0;
|
|
562
|
-
for (const
|
|
563
|
-
const
|
|
564
|
-
if (rnd < 1)
|
|
565
|
-
continue;
|
|
566
|
-
const view = decodeRound(entry);
|
|
567
|
-
const violations = view
|
|
568
|
-
? view.constraintViolations ?? []
|
|
569
|
-
: Array.isArray(entry.constraint_violations)
|
|
570
|
-
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
571
|
-
: [];
|
|
572
|
-
for (const v of violations) {
|
|
313
|
+
for (const view of rounds) {
|
|
314
|
+
for (const v of view.constraintViolations ?? []) {
|
|
573
315
|
if (matchesConstraintText(text, v)) {
|
|
574
|
-
if (
|
|
575
|
-
latest =
|
|
316
|
+
if (view.round > latest)
|
|
317
|
+
latest = view.round;
|
|
576
318
|
break;
|
|
577
319
|
}
|
|
578
320
|
}
|
|
@@ -590,18 +332,10 @@ function findLastViolatedRound(text, vaultEntries) {
|
|
|
590
332
|
* and last_violated_at_round for the L1 collapse "(violated this round)"
|
|
591
333
|
* annotation (diffConstraints compares it to the current round).
|
|
592
334
|
*/
|
|
593
|
-
function manageConstraintLifecycle(active,
|
|
594
|
-
const allEntries = loopEntries(objective.loop_id || "", vaultContext);
|
|
595
|
-
const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
|
|
335
|
+
function manageConstraintLifecycle(active, provenance, windowRounds) {
|
|
596
336
|
const metadata = [];
|
|
597
337
|
for (const text of active) {
|
|
598
|
-
const source =
|
|
599
|
-
// v3.2.1: a violation reported in THIS round (last_round_result) counts
|
|
600
|
-
// as a violation at currentRound — the previous rounds' scan cannot see
|
|
601
|
-
// it (this round is not committed yet). This makes the L1 collapse
|
|
602
|
-
// render "(violated this round)" (diffConstraints compares
|
|
603
|
-
// last_violated_at_round === round).
|
|
604
|
-
const violatedThisRound = lastRoundViolations.some((v) => matchesConstraintText(text, v));
|
|
338
|
+
const source = provenance.get(text) ?? { source: "unrecorded" };
|
|
605
339
|
// v3.3.1: one text, one identity — a success-criterion text that also
|
|
606
340
|
// lives in the merged active set belongs to the cr-XXXXXXXX namespace
|
|
607
341
|
// (its own Success Criteria section and the verification gate's
|
|
@@ -610,34 +344,27 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
|
|
|
610
344
|
// and an agent echoing the active-list ID into its criterion claims
|
|
611
345
|
// could never match.
|
|
612
346
|
metadata.push(makeConstraintMeta({
|
|
613
|
-
id: source === "criteria" ? deriveCriterionId(text) : deriveConstraintId(text),
|
|
347
|
+
id: source.source === "criteria" ? deriveCriterionId(text) : deriveConstraintId(text),
|
|
614
348
|
text,
|
|
615
|
-
last_violated_at_round:
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
source,
|
|
349
|
+
last_violated_at_round: findLastViolatedRound(text, windowRounds),
|
|
350
|
+
source: source.source,
|
|
351
|
+
...(source.source_round !== undefined ? { source_round: source.source_round } : {}),
|
|
619
352
|
}));
|
|
620
353
|
}
|
|
621
354
|
return { active, metadata };
|
|
622
355
|
}
|
|
623
356
|
/** Resolve the ONE previous-round fact bundle for a compile.
|
|
624
357
|
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const rounds = windowRounds ??
|
|
632
|
-
derivationRounds(loopEntries(request.loop_id, context), request.round);
|
|
633
|
-
const view = rounds.find((round) => round.round === request.round - 1) ?? null;
|
|
634
|
-
const recoveryBoundary = pendingBacktrackRecord(loopEntries(request.loop_id, context), request.round) !== null;
|
|
358
|
+
* It is built from the committed window only. A retry's verification findings
|
|
359
|
+
* are authoritative even when empty: the retry must never inherit the
|
|
360
|
+
* previous committed round's flags. */
|
|
361
|
+
function resolvePreviousRoundFacts(request, entries, windowRounds) {
|
|
362
|
+
const view = windowRounds.find((round) => round.round === request.round - 1) ?? null;
|
|
363
|
+
const recoveryBoundary = pendingBacktrackRecord(entries, request.round) !== null;
|
|
635
364
|
const inFlight = (request.attempt ?? 1) > 1 || recoveryBoundary;
|
|
636
365
|
return {
|
|
637
366
|
view,
|
|
638
|
-
result: view
|
|
639
|
-
? roundResultFromCommittedRound(view)
|
|
640
|
-
: request.last_round_result,
|
|
367
|
+
result: view ? roundResultFromCommittedRound(view) : null,
|
|
641
368
|
verificationFlags: inFlight
|
|
642
369
|
? (request.verification_flags ?? [])
|
|
643
370
|
: (view?.verificationFlags ?? request.verification_flags ?? []),
|
|
@@ -645,32 +372,45 @@ function resolvePreviousRoundFacts(request, context, windowRounds) {
|
|
|
645
372
|
recoveryBoundary,
|
|
646
373
|
};
|
|
647
374
|
}
|
|
648
|
-
function levelDecision(request, prior, previous) {
|
|
375
|
+
function levelDecision(request, prior, previous, goalId) {
|
|
649
376
|
return decidePromptLevel({
|
|
650
377
|
round: request.round,
|
|
651
378
|
attempt: request.attempt,
|
|
652
379
|
forceLevel: request.force_level,
|
|
653
380
|
hasPlanSource: Boolean(request.plan_source),
|
|
654
381
|
recoveryBoundary: previous.recoveryBoundary,
|
|
655
|
-
|
|
656
|
-
// An empty goal_id is unobservable, not a goal change: never
|
|
382
|
+
subtaskBoundary: previous.result?.subtask_boundary === true,
|
|
383
|
+
// An empty prior goal_id is unobservable, not a goal change: never
|
|
384
|
+
// fabricate L2. `goalId` is derived from the working objective, so an
|
|
385
|
+
// objective_refinement legitimately changes it.
|
|
657
386
|
goalChanged: prior !== null && prior.goal_id !== "" &&
|
|
658
|
-
prior.goal_id !==
|
|
387
|
+
prior.goal_id !== goalId,
|
|
659
388
|
previousStateMissing: request.round > 1 && prior === null,
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
?
|
|
389
|
+
// v4.0.3: prompt density consumes one explicit previous-round bundle.
|
|
390
|
+
// Agent outcome remains semantic input; posture/finding facts remain
|
|
391
|
+
// machine input. The policy owns the combination so callers cannot drift.
|
|
392
|
+
previousRound: previous.view
|
|
393
|
+
? {
|
|
394
|
+
agentOutcome: previous.view.agentOutcome,
|
|
395
|
+
evidencePosture: previous.view.evidencePosture?.status ?? null,
|
|
396
|
+
observableNewInformation: previous.freshness.observableNewInformation,
|
|
397
|
+
hasVerificationError: previous.verificationFlags.some((flag) => flag.severity === "error"),
|
|
398
|
+
}
|
|
665
399
|
: null,
|
|
666
|
-
verificationContradicted: previous.verificationFlags
|
|
667
|
-
.some((flag) => flag.severity === "error"),
|
|
668
400
|
consecutiveRejections: request.consecutive_rejections,
|
|
401
|
+
// v4.0.5: a cold handoff rehydrates at L2; the escalation threshold above
|
|
402
|
+
// still wins because that retry is already clean context.
|
|
403
|
+
invocationRecovery: request.invocation_recovery === true,
|
|
669
404
|
});
|
|
670
405
|
}
|
|
671
|
-
export function decideLevel(request, context) {
|
|
672
|
-
const
|
|
673
|
-
|
|
406
|
+
export function decideLevel(request, context, workingObjective) {
|
|
407
|
+
const entries = contextEntries(context);
|
|
408
|
+
const history = buildCommittedHistory(entries);
|
|
409
|
+
const window = historyWindow(history, request.round);
|
|
410
|
+
const prior = previousRoundFromWindow(window, request.round - 1);
|
|
411
|
+
const objective = workingObjective ??
|
|
412
|
+
latestObjective(request.loop_id, context)?.objective ?? request.task;
|
|
413
|
+
return levelDecision(request, prior, resolvePreviousRoundFacts(request, entries, window), deriveGoalId(request.loop_id, objective)).level;
|
|
674
414
|
}
|
|
675
415
|
/** The prompt's evaluation block. All content (examples, contract/sub-goal
|
|
676
416
|
* guidance, flag-specific hints) comes from the one FIELD_GUIDES table in
|
|
@@ -678,82 +418,105 @@ export function decideLevel(request, context) {
|
|
|
678
418
|
export function buildSelfEvalBlock(ctx) {
|
|
679
419
|
return renderSelfEvalBlock(ctx);
|
|
680
420
|
}
|
|
681
|
-
// v2.5: checkpoint() removed — agent_declared milestones from
|
|
682
|
-
// compression_checkpoint supersede the separate CheckpointSummary.
|
|
683
|
-
// See buildRollingSummary() signal 1.
|
|
684
421
|
/** v3.7.1: Derived Recovery Brief for the state-file Recent tier. Present
|
|
685
422
|
* ONLY while a committed backtrack decision is the current round's record
|
|
686
423
|
* (the recovery window); the redo commit replaces that record, so the
|
|
687
424
|
* brief exits the projection by construction. Sources are the committed
|
|
688
425
|
* rollback decision's own fields — no new persistence, no rejected
|
|
689
426
|
* payloads. */
|
|
690
|
-
function committedBacktrackBrief(loopId, round, context) {
|
|
427
|
+
function committedBacktrackBrief(loopId, round, context, restoreVerified) {
|
|
691
428
|
// The SAME pending-backtrack lookup the recovery boundary uses: a deep
|
|
692
429
|
// restore commits at the failed round while its redo targets
|
|
693
430
|
// `backtrackTarget + 1`, so an exact-round match loses both the boundary
|
|
694
431
|
// and this brief.
|
|
695
|
-
const view = pendingBacktrackRecord(
|
|
696
|
-
const record = view?.
|
|
432
|
+
const view = pendingBacktrackRecord(contextEntries(context), round);
|
|
433
|
+
const record = view?.recovery;
|
|
697
434
|
if (!record)
|
|
698
435
|
return undefined;
|
|
699
436
|
const lines = recoveryBriefLines({
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
437
|
+
redoRound: record.target.redo_round,
|
|
438
|
+
checkpointId: record.target.checkpoint_id,
|
|
439
|
+
restoreVerified,
|
|
440
|
+
triggerRule: record.origin === "machine"
|
|
441
|
+
? (record.trigger_rule ?? "machine_recovery")
|
|
442
|
+
: "agent_recovery",
|
|
443
|
+
origin: record.origin,
|
|
444
|
+
reason: record.reason,
|
|
445
|
+
...(record.origin === "machine" && record.reason.trim()
|
|
446
|
+
? { failureReason: record.reason }
|
|
707
447
|
: {}),
|
|
708
|
-
failedRounds: record.
|
|
448
|
+
failedRounds: record.failed_rounds,
|
|
709
449
|
approaches: record.approaches,
|
|
710
|
-
wrongAssumptions: record.
|
|
450
|
+
wrongAssumptions: record.wrong_assumptions,
|
|
451
|
+
preservedDiscoveries: record.skipped_discoveries,
|
|
711
452
|
// The redo round's stable identity is a fact the old prepended prose
|
|
712
453
|
// carried; keep it inside the artifact now.
|
|
713
|
-
recoveryRoundId: `loop:${loopId}:round:${record.target
|
|
454
|
+
recoveryRoundId: `loop:${loopId}:round:${record.target.redo_round}`,
|
|
714
455
|
});
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if (
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
if (record.skippedFiles.length > 0) {
|
|
722
|
-
const files = [...record.skippedFiles].sort();
|
|
723
|
-
const shown = files.slice(0, 15);
|
|
724
|
-
lines.push("- **Files changed in failed rounds**:");
|
|
456
|
+
const observedFiles = [...(record.observed_skipped_files ??
|
|
457
|
+
record.skipped_files ?? [])].sort();
|
|
458
|
+
const reportedFiles = [...(record.reported_skipped_files ?? [])].sort();
|
|
459
|
+
if (observedFiles.length > 0) {
|
|
460
|
+
const shown = observedFiles.slice(0, 15);
|
|
461
|
+
lines.push("- **Observed changes since the checkpoint (machine)**:");
|
|
725
462
|
for (const file of shown)
|
|
726
463
|
lines.push(` - \`${file}\``);
|
|
727
|
-
if (
|
|
728
|
-
lines.push(` - … and ${
|
|
464
|
+
if (observedFiles.length > shown.length) {
|
|
465
|
+
lines.push(` - … and ${observedFiles.length - shown.length} more files`);
|
|
729
466
|
}
|
|
730
467
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
`
|
|
736
|
-
|
|
737
|
-
|
|
468
|
+
if (reportedFiles.length > 0) {
|
|
469
|
+
const shown = reportedFiles.slice(0, 15);
|
|
470
|
+
lines.push("- **Agent-reported changed files (report)**:");
|
|
471
|
+
for (const file of shown)
|
|
472
|
+
lines.push(` - \`${file}\``);
|
|
473
|
+
if (reportedFiles.length > shown.length) {
|
|
474
|
+
lines.push(` - … and ${reportedFiles.length - shown.length} more files`);
|
|
475
|
+
}
|
|
738
476
|
}
|
|
739
477
|
return lines;
|
|
740
478
|
}
|
|
741
|
-
export function compileLoop(request, context) {
|
|
479
|
+
export function compileLoop(request, context, options = {}, committedHistory) {
|
|
480
|
+
const operatorAcceptance = options.operatorAcceptance ?? null;
|
|
742
481
|
// ONE history window and ONE previous-round fact bundle for the whole
|
|
743
|
-
// compile. Every consumer below reads these values; none re-
|
|
744
|
-
//
|
|
745
|
-
const
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
482
|
+
// compile. Every consumer below reads these values; none re-derives the
|
|
483
|
+
// window with a private rule.
|
|
484
|
+
const entries = contextEntries(context);
|
|
485
|
+
const history = committedHistory ?? buildCommittedHistory(entries);
|
|
486
|
+
if (history.legacyRounds.length > 0) {
|
|
487
|
+
throw new LegacyHistoryError(request.loop_id, history.legacyRounds.map((round) => round.round));
|
|
488
|
+
}
|
|
489
|
+
// The operator surface is immutable. A caller-supplied copy that disagrees
|
|
490
|
+
// with a committed copy is an integrity conflict — never a silent override.
|
|
491
|
+
const committedAcceptance = history.views.find((view) => view.operatorAcceptance !== null)?.operatorAcceptance ?? null;
|
|
492
|
+
if (operatorAcceptance && committedAcceptance &&
|
|
493
|
+
operatorAcceptanceKey(operatorAcceptance) !==
|
|
494
|
+
operatorAcceptanceKey(committedAcceptance)) {
|
|
495
|
+
throw new OperatorAcceptanceConflictError(request.loop_id);
|
|
496
|
+
}
|
|
497
|
+
const windowRounds = historyWindow(history, request.round);
|
|
498
|
+
const prior = previousRoundFromWindow(windowRounds, request.round - 1);
|
|
499
|
+
const previous = resolvePreviousRoundFacts(request, entries, windowRounds);
|
|
500
|
+
// v4.0.6: the working objective is computed BEFORE the level decision —
|
|
501
|
+
// a committed objective_refinement changes goal identity, and that change
|
|
502
|
+
// must be able to select L2.
|
|
503
|
+
const objective = evolveObjective(request, context, operatorAcceptance);
|
|
504
|
+
const goalId = deriveGoalId(request.loop_id, objective.objective);
|
|
505
|
+
const decision = levelDecision(request, prior, previous, goalId);
|
|
506
|
+
const pendingDirective = pendingBacktrackRecord(entries, request.round);
|
|
507
|
+
const preservedDiscoveries = pendingDirective?.recovery &&
|
|
508
|
+
getPolicy().engine.backtrack_preserve_discoveries
|
|
509
|
+
? pendingDirective.recovery.skipped_discoveries
|
|
510
|
+
: [];
|
|
511
|
+
const preservedRound = pendingDirective?.round;
|
|
512
|
+
// Plan constraints are a round-1 operator/plan input, or a library
|
|
513
|
+
// bootstrap when no committed round exists. Once a committed view exists,
|
|
514
|
+
// the committed active set carries them and a late request is not a fact.
|
|
515
|
+
const planInput = request.round === 1 || previous.view === null
|
|
516
|
+
? request.constraints_from_plan
|
|
517
|
+
: [];
|
|
518
|
+
const constraints = evolveConstraints(request, objective, previous, windowRounds, planInput, preservedDiscoveries, preservedRound);
|
|
519
|
+
const constraintLifecycle = manageConstraintLifecycle(constraints.active, constraints.provenance, windowRounds);
|
|
757
520
|
// v3.8: sub-goal lifecycle lives in subgoal-state.ts and consumes the
|
|
758
521
|
// shared committed-round view — one history interpretation. The previous
|
|
759
522
|
// round's report is idempotently replayed; a committed round is already in
|
|
@@ -762,31 +525,21 @@ export function compileLoop(request, context) {
|
|
|
762
525
|
loopId: request.loop_id,
|
|
763
526
|
currentRound: request.round,
|
|
764
527
|
rounds: windowRounds,
|
|
765
|
-
currentReport: previous.result
|
|
766
|
-
? {
|
|
767
|
-
round: previous.result.round || (request.round - 1),
|
|
768
|
-
emergedSubtasks: previous.result.emerged_subtasks ?? [],
|
|
769
|
-
subgoalUpdates: previous.result.subgoal_updates ?? [],
|
|
770
|
-
}
|
|
771
|
-
: null,
|
|
772
528
|
});
|
|
773
|
-
// v3.4/v3.8/v3.8.1: the ACTIVE Round Contract
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
//
|
|
529
|
+
// v3.4/v3.8/v3.8.1: the ACTIVE Round Contract and its per-item statuses for
|
|
530
|
+
// this compile — ONE `deriveRoundFacts` call, the same bundle the projection
|
|
531
|
+
// consumes. Derived from committed rounds, so rejection retries, resume,
|
|
532
|
+
// unpause and backtrack compiles keep showing the same contract, and a
|
|
533
|
+
// verified or blocked contract stops being shown. A verified item stays
|
|
534
|
+
// proven after its contract closes because the bundle reads the whole
|
|
535
|
+
// history, not the active contract. The compile path has no in-flight
|
|
536
|
+
// observations of its own.
|
|
781
537
|
const roundFacts = deriveRoundFacts({
|
|
782
538
|
rounds: windowRounds,
|
|
783
539
|
currentRound: request.round,
|
|
784
540
|
inFlight: NO_IN_FLIGHT_ROUND,
|
|
785
|
-
subGoals,
|
|
786
541
|
commands: getPolicy().evidence.commands ?? [],
|
|
787
542
|
});
|
|
788
|
-
// `roundFacts.verifiedSubGoals` is not read here — it reaches the state file
|
|
789
|
-
// and the projection through the bundle itself.
|
|
790
543
|
const { activeContract } = roundFacts;
|
|
791
544
|
// v3.9.4: the criterion reference space is the ACTIVE criterion set, not
|
|
792
545
|
// every text the objective ever carried. A retracted criterion must not be
|
|
@@ -809,13 +562,27 @@ export function compileLoop(request, context) {
|
|
|
809
562
|
// gone with the rest of the fuzzy matching, and so are the task-alignment
|
|
810
563
|
// and loop-health warnings: both were Jaccard scores over task/objective
|
|
811
564
|
// text, never machine facts.
|
|
812
|
-
|
|
565
|
+
// v3.9.5: only committed declarations are history. A library fallback is
|
|
566
|
+
// in-flight and must not manufacture a prompt warning.
|
|
567
|
+
const repeatedDeclarations = previous.view
|
|
568
|
+
? duplicateEmergedDeclarations(previous.view.emergedSubtasks ?? [])
|
|
569
|
+
: [];
|
|
813
570
|
const warnings = unique([
|
|
814
571
|
repeatedDeclarations.length > 0
|
|
815
572
|
? `duplicate_declaration: ${repeatedDeclarations.length} sub-goal ` +
|
|
816
573
|
`declaration(s) repeated an earlier entry in the same round and were ` +
|
|
817
574
|
`not created: ${repeatedDeclarations.join("; ")}`
|
|
818
575
|
: "",
|
|
576
|
+
request.round > 1 && previous.view !== null && request.loop_objective !== null
|
|
577
|
+
? "uncommitted_objective_ignored: a committed objective exists; the request objective is ignored"
|
|
578
|
+
: "",
|
|
579
|
+
request.round > 1 && previous.view !== null && request.constraints_from_plan.length > 0
|
|
580
|
+
? "uncommitted_plan_ignored: a committed active set exists; request plan constraints are ignored"
|
|
581
|
+
: "",
|
|
582
|
+
constraints.discoveryDropped > 0
|
|
583
|
+
? `discovery_cap: ${constraints.discoveryDropped} discovered constraint(s) exceeded ` +
|
|
584
|
+
`max_discovered_constraints_per_round and were not expanded`
|
|
585
|
+
: "",
|
|
819
586
|
]);
|
|
820
587
|
// v3.8.1: the agent trust score and trend are deleted. The score was
|
|
821
588
|
// `1 - errors*0.15 - warns*0.03` — arbitrary weights with no machine
|
|
@@ -830,20 +597,28 @@ export function compileLoop(request, context) {
|
|
|
830
597
|
// (and therefore into stateHash) and read by nothing. `windowRounds` is the
|
|
831
598
|
// ONE window computed at the top of this function.
|
|
832
599
|
const machineStatus = deriveMachineStatus(windowRounds, request.round);
|
|
600
|
+
// v4.0.2: ONE derivation of the bounded Working Memory and the context
|
|
601
|
+
// boundary for the whole compile. State, prompt, artifact and every
|
|
602
|
+
// prompt-bearing response project these values; none re-derives them.
|
|
603
|
+
const workingMemory = deriveWorkingMemory(history, request.round);
|
|
604
|
+
const contextBoundary = derivePromptContextBoundary({
|
|
605
|
+
round: request.round,
|
|
606
|
+
attempt: request.attempt,
|
|
607
|
+
consecutiveRejections: request.consecutive_rejections,
|
|
608
|
+
recoveryBoundary: previous.recoveryBoundary,
|
|
609
|
+
hasPreviousCommittedRound: previous.view !== null,
|
|
610
|
+
invocationRecovery: request.invocation_recovery === true,
|
|
611
|
+
});
|
|
833
612
|
const response = makeLoopCompileResponse({
|
|
834
613
|
status: AgentStatus.OK,
|
|
835
614
|
recompile_level: decision.level,
|
|
836
|
-
diff_from_previous: decision.reasons.join(","),
|
|
837
|
-
lineage: [`${request.loop_id}:r${request.round}`],
|
|
838
615
|
constraints_active: constraintLifecycle.active,
|
|
839
616
|
constraints_retired: constraints.retired,
|
|
840
617
|
constraint_metadata: constraintLifecycle.metadata,
|
|
841
618
|
loop_id: request.loop_id,
|
|
842
619
|
round: request.round,
|
|
843
|
-
goal_id:
|
|
844
|
-
goal_text_hash: computeGoalTextHash(request.task),
|
|
620
|
+
goal_id: goalId,
|
|
845
621
|
loop_objective: objective,
|
|
846
|
-
rolling_summary: rolling,
|
|
847
622
|
sub_goals: subGoals,
|
|
848
623
|
criterion_statuses: criterionStatuses,
|
|
849
624
|
recurring_flags: recurringFlags,
|
|
@@ -858,6 +633,10 @@ export function compileLoop(request, context) {
|
|
|
858
633
|
const statePath = policy.state_file.enabled
|
|
859
634
|
? `${policy.state_file.directory}/${request.loop_id}-state.md`
|
|
860
635
|
: "";
|
|
636
|
+
// The recovery brief is a current-round fact, computed once and shared by
|
|
637
|
+
// the state file, the prompt and the evaluation block. It never becomes
|
|
638
|
+
// prompt control state.
|
|
639
|
+
const recoveryBrief = committedBacktrackBrief(request.loop_id, request.round, context, request.restore_verified === true);
|
|
861
640
|
const state = createCanonicalLoopState(request, response, statePath, {
|
|
862
641
|
machineStatus,
|
|
863
642
|
// v3.4/v3.8.1: the contract-fact bundle — the Current Task's source of
|
|
@@ -865,14 +644,20 @@ export function compileLoop(request, context) {
|
|
|
865
644
|
// ONE derivation. The static capability rides on the state itself.
|
|
866
645
|
roundFacts,
|
|
867
646
|
// 3.9.3: the same previous-round bundle the level decision used. This is
|
|
868
|
-
// what keeps
|
|
869
|
-
//
|
|
870
|
-
|
|
647
|
+
// what keeps corrections/blockers/remaining/progress identical on live,
|
|
648
|
+
// resume, and unpause compiles.
|
|
649
|
+
// v3.9.5: the committed flag is what keeps a caller fallback out of the
|
|
650
|
+
// prompt/state narrative.
|
|
651
|
+
previousRound: { ...previous, committed: previous.view !== null },
|
|
652
|
+
recoveryBrief,
|
|
653
|
+
workingMemory,
|
|
654
|
+
context: {
|
|
655
|
+
round: request.round,
|
|
656
|
+
attempt: request.attempt,
|
|
657
|
+
mode: contextBoundary.mode,
|
|
658
|
+
reason: contextBoundary.reason,
|
|
659
|
+
},
|
|
871
660
|
});
|
|
872
|
-
// The recovery brief is a current-round fact, computed once and shared by
|
|
873
|
-
// the state file and the evaluation block. It never becomes prompt control
|
|
874
|
-
// state.
|
|
875
|
-
const recoveryBrief = committedBacktrackBrief(request.loop_id, request.round, context);
|
|
876
661
|
const configuredCapability = deriveConfiguredCapability(policy);
|
|
877
662
|
const contractCommands = configuredCapability.commands
|
|
878
663
|
.filter((command) => command.enabled)
|
|
@@ -884,6 +669,12 @@ export function compileLoop(request, context) {
|
|
|
884
669
|
// round's record; the redo commit removes it by construction.
|
|
885
670
|
attempt: request.attempt,
|
|
886
671
|
recoveryBrief,
|
|
672
|
+
context: {
|
|
673
|
+
round: request.round,
|
|
674
|
+
attempt: request.attempt,
|
|
675
|
+
mode: contextBoundary.mode,
|
|
676
|
+
reason: contextBoundary.reason,
|
|
677
|
+
},
|
|
887
678
|
});
|
|
888
679
|
// v3.8.1: the L2 budget is a FIXED ceiling. The v2.4 adaptive budget
|
|
889
680
|
// scaled it by round count, milestone count and sub-goal count, which made
|
|
@@ -891,25 +682,22 @@ export function compileLoop(request, context) {
|
|
|
891
682
|
// rather than on the facts of this round — the same loop could render a
|
|
892
683
|
// section at round 5 and silently drop it at round 50.
|
|
893
684
|
//
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
// L7: pointer mode REQUIRES the file — with state_file disabled it would
|
|
898
|
-
// silently drop the full state from L2 prompts and point at nothing.
|
|
899
|
-
const l2Pointer = policy.state_file.enabled &&
|
|
900
|
-
policy.prompt.l2_pointer_enabled && decision.level === "l2";
|
|
685
|
+
// v3.9.5: L2 never embeds the monolithic state markdown. The prompt is a
|
|
686
|
+
// current-governance projection; full history stays in the optional state
|
|
687
|
+
// file / replay / audit.
|
|
901
688
|
const artifact = assemblePromptArtifact({
|
|
902
689
|
state,
|
|
903
690
|
level: decision.level,
|
|
904
|
-
reasons: decision.reasons,
|
|
905
691
|
budgets: {
|
|
906
692
|
l0: policy.prompt.l0_max_chars,
|
|
907
693
|
l1: policy.prompt.l1_max_chars,
|
|
908
694
|
l2: policy.prompt.l2_max_chars,
|
|
909
695
|
},
|
|
910
696
|
attempt: request.attempt,
|
|
697
|
+
warnings,
|
|
911
698
|
selfEvaluationBlock: buildSelfEvalBlock({
|
|
912
699
|
round: request.round,
|
|
700
|
+
maxRounds: request.max_rounds ?? policy.engine.max_rounds,
|
|
913
701
|
level: decision.level,
|
|
914
702
|
activeContract,
|
|
915
703
|
itemStatuses: roundFacts.itemStatuses,
|
|
@@ -920,19 +708,34 @@ export function compileLoop(request, context) {
|
|
|
920
708
|
recovery: recoveryBrief !== undefined,
|
|
921
709
|
gateEnabled: policy.gate.enabled === true,
|
|
922
710
|
criteria: criterionStatuses,
|
|
923
|
-
currentRequirement: state.currentTask,
|
|
924
711
|
}),
|
|
925
|
-
fullStateMarkdown: l2Pointer ? undefined : markdown,
|
|
926
712
|
// v3.9.4: the SAME committed-backtrack facts the state file renders. The
|
|
927
713
|
// recovery section is part of the artifact, so the delivered prompt is
|
|
928
714
|
// exactly `artifact.renderedPrompt` (no post-compile prepend).
|
|
929
715
|
recoveryBrief,
|
|
930
|
-
//
|
|
931
|
-
|
|
716
|
+
// v4.0.2: the ONE context boundary and the SAME bounded Working Memory
|
|
717
|
+
// the state carries. A retry renders no Working Memory section.
|
|
718
|
+
contextMode: contextBoundary.mode,
|
|
719
|
+
contextReason: contextBoundary.reason,
|
|
720
|
+
workingMemory,
|
|
932
721
|
});
|
|
933
722
|
response.prompt = artifact.renderedPrompt;
|
|
934
723
|
response.prompt_artifact = artifact;
|
|
935
724
|
response.state_file_content = policy.state_file.enabled ? markdown : undefined;
|
|
725
|
+
// v4.0.6: the fallback acceptance uses the immutable facts, never the
|
|
726
|
+
// working objective text: `objective.objective` may be an Agent refinement
|
|
727
|
+
// and must not be written back as Operator Acceptance.
|
|
728
|
+
response.operatorAcceptance = operatorAcceptance ?? committedAcceptance ?? {
|
|
729
|
+
task: request.task,
|
|
730
|
+
success_criteria: [...objective.success_criteria],
|
|
731
|
+
hard_constraints: [...objective.hard_constraints],
|
|
732
|
+
};
|
|
733
|
+
response.compiled = {
|
|
734
|
+
task: objective.objective,
|
|
735
|
+
goalId: response.goal_id,
|
|
736
|
+
constraintsActive: [...constraintLifecycle.active],
|
|
737
|
+
constraintsRetired: [...constraints.retired],
|
|
738
|
+
};
|
|
936
739
|
return response;
|
|
937
740
|
}
|
|
938
741
|
//# sourceMappingURL=loop-compiler.js.map
|