loopforge 2.7.0 → 2.11.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 +103 -166
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +44 -6
- package/dist/canonical-state.js.map +1 -1
- package/dist/enforcement-gate.d.ts +22 -1
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +281 -38
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +21 -3
- package/dist/engine.js.map +1 -1
- package/dist/evidence-provider.d.ts +3 -5
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +24 -10
- package/dist/evidence-provider.js.map +1 -1
- package/dist/index.d.ts +2 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -11
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +2 -2
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +135 -42
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +3 -5
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +42 -8
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +27 -2
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +145 -1
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +26 -4
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +8 -0
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +11 -0
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +46 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +12 -2
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +3 -1
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +427 -29
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/protocol.d.ts +54 -8
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +7 -1
- package/dist/protocol.js.map +1 -1
- package/dist/round-coordinator.d.ts +28 -4
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +91 -7
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +4 -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 +4 -0
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +4 -5
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +30 -0
- package/dist/self-eval.js.map +1 -1
- package/dist/token-utils.d.ts +3 -0
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +6 -0
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +6 -2
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +41 -5
- package/dist/verification-gate.js.map +1 -1
- package/package.json +2 -2
- package/skills/perception/SKILL.md +105 -30
- package/dist/interop.d.ts +0 -35
- package/dist/interop.d.ts.map +0 -1
- package/dist/interop.js +0 -34
- package/dist/interop.js.map +0 -1
- package/dist/run.d.ts +0 -51
- package/dist/run.d.ts.map +0 -1
- package/dist/run.js +0 -200
- package/dist/run.js.map +0 -1
package/dist/prompt-assembler.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { hashCanonicalState } from "./canonical-state.js";
|
|
9
|
+
import { jaccardSimilarity } from "./token-utils.js";
|
|
10
|
+
import { getPolicy } from "./policy.js";
|
|
9
11
|
export const PROMPT_ARTIFACT_SCHEMA_VERSION = 1;
|
|
10
12
|
export const BASE_PROMPT_VERSION = "2.0.0";
|
|
11
13
|
export const DEFAULT_PROMPT_BUDGETS = {
|
|
@@ -19,6 +21,29 @@ function section(title, body) {
|
|
|
19
21
|
function bullets(values, prefix = "- ") {
|
|
20
22
|
return values.map((value) => `${prefix}${value}`).join("\n");
|
|
21
23
|
}
|
|
24
|
+
/** v2.11: Derive a stable item ID from text content.
|
|
25
|
+
* Same hash strategy as computeGoalTextHash in loop-compiler.ts. */
|
|
26
|
+
function deriveItemId(text) {
|
|
27
|
+
const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
|
|
28
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 8);
|
|
29
|
+
}
|
|
30
|
+
/** v2.11: Render a bullet list with ID prefixes for constraints/criteria.
|
|
31
|
+
* Each item gets a [prefix-XXXXXXXX] tag. Items that already look like IDs
|
|
32
|
+
* are rendered as-is. When constraint_id_enabled is false, renders plain bullets. */
|
|
33
|
+
function bulletsWithIds(values, prefix) {
|
|
34
|
+
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
35
|
+
if (!idEnabled)
|
|
36
|
+
return bullets(values);
|
|
37
|
+
const idRegex = /^(c|cr|sg)-[a-f0-9]{8}$/;
|
|
38
|
+
return values.map((v) => {
|
|
39
|
+
const trimmed = v.trim();
|
|
40
|
+
if (idRegex.test(trimmed)) {
|
|
41
|
+
return `- [\`${trimmed}\`] ${trimmed}`;
|
|
42
|
+
}
|
|
43
|
+
const id = prefix + "-" + deriveItemId(trimmed);
|
|
44
|
+
return `- [\`${id}\`] ${trimmed}`;
|
|
45
|
+
}).join("\n");
|
|
46
|
+
}
|
|
22
47
|
function verificationText(state) {
|
|
23
48
|
const lines = state.verificationFlags.map((flag) => {
|
|
24
49
|
const icon = flag.severity === "error"
|
|
@@ -35,7 +60,9 @@ function activeNonHardConstraints(state) {
|
|
|
35
60
|
const hard = new Set(state.hardConstraints);
|
|
36
61
|
return state.activeConstraints.filter((value) => !hard.has(value));
|
|
37
62
|
}
|
|
38
|
-
function commonMandatorySections(state
|
|
63
|
+
function commonMandatorySections(state,
|
|
64
|
+
/** v2.11: L0 skips ID rendering to stay within tight budget. */
|
|
65
|
+
level) {
|
|
39
66
|
const sections = [
|
|
40
67
|
{
|
|
41
68
|
id: "objective",
|
|
@@ -49,9 +76,13 @@ function commonMandatorySections(state) {
|
|
|
49
76
|
},
|
|
50
77
|
];
|
|
51
78
|
if (state.hardConstraints.length > 0) {
|
|
79
|
+
// v2.11: L0 uses plain bullets (tight budget); L1/L2 show constraint IDs
|
|
80
|
+
const body = level === "l0"
|
|
81
|
+
? bullets(state.hardConstraints)
|
|
82
|
+
: bulletsWithIds(state.hardConstraints, "c");
|
|
52
83
|
sections.push({
|
|
53
84
|
id: "hard_constraints",
|
|
54
|
-
text: section("Active Hard Constraints",
|
|
85
|
+
text: section("Active Hard Constraints", body),
|
|
55
86
|
mandatory: true,
|
|
56
87
|
});
|
|
57
88
|
}
|
|
@@ -69,7 +100,7 @@ function l0Sections(state) {
|
|
|
69
100
|
...state.blockers,
|
|
70
101
|
...state.verificationFlags.map((flag) => flag.detail),
|
|
71
102
|
];
|
|
72
|
-
const sections = commonMandatorySections(state);
|
|
103
|
+
const sections = commonMandatorySections(state, "l0");
|
|
73
104
|
if (retryRequirements.length > 0) {
|
|
74
105
|
sections.push({
|
|
75
106
|
id: "retry_requirements",
|
|
@@ -87,7 +118,7 @@ function l0Sections(state) {
|
|
|
87
118
|
return sections;
|
|
88
119
|
}
|
|
89
120
|
function l1Sections(state) {
|
|
90
|
-
const sections = commonMandatorySections(state);
|
|
121
|
+
const sections = commonMandatorySections(state, "l1");
|
|
91
122
|
const active = activeNonHardConstraints(state);
|
|
92
123
|
if (state.changesSinceLastRound.length > 0) {
|
|
93
124
|
sections.push({
|
|
@@ -99,14 +130,14 @@ function l1Sections(state) {
|
|
|
99
130
|
if (active.length > 0) {
|
|
100
131
|
sections.push({
|
|
101
132
|
id: "active_constraints",
|
|
102
|
-
text: section("Active Constraints / Success Criteria",
|
|
133
|
+
text: section("Active Constraints / Success Criteria", bulletsWithIds(active, "c")),
|
|
103
134
|
mandatory: false,
|
|
104
135
|
});
|
|
105
136
|
}
|
|
106
137
|
if (state.remainingCriteria.length > 0) {
|
|
107
138
|
sections.push({
|
|
108
139
|
id: "remaining",
|
|
109
|
-
text: section("Remaining",
|
|
140
|
+
text: section("Remaining", bulletsWithIds(state.remainingCriteria, "cr")),
|
|
110
141
|
mandatory: false,
|
|
111
142
|
});
|
|
112
143
|
}
|
|
@@ -123,7 +154,7 @@ function l1Sections(state) {
|
|
|
123
154
|
const maxShow = 5;
|
|
124
155
|
const items = activeSubs.slice(0, maxShow).map((sg) => {
|
|
125
156
|
const icon = sg.status === "in_progress" ? "🔄" : "⏳";
|
|
126
|
-
return `${icon} ${sg.description}`;
|
|
157
|
+
return `${icon} [\`${sg.id}\`] ${sg.description}`;
|
|
127
158
|
});
|
|
128
159
|
if (activeSubs.length > maxShow) {
|
|
129
160
|
items.push(`... and ${activeSubs.length - maxShow} more`);
|
|
@@ -165,7 +196,7 @@ function l1Sections(state) {
|
|
|
165
196
|
return sections;
|
|
166
197
|
}
|
|
167
198
|
function l2Sections(state, fullStateMarkdown) {
|
|
168
|
-
const sections = commonMandatorySections(state);
|
|
199
|
+
const sections = commonMandatorySections(state, "l2");
|
|
169
200
|
const fullState = fullStateMarkdown?.trim();
|
|
170
201
|
if (fullState) {
|
|
171
202
|
sections.push({
|
|
@@ -175,20 +206,24 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
175
206
|
});
|
|
176
207
|
return sections;
|
|
177
208
|
}
|
|
209
|
+
// v2.11: Groups with an idPrefix use ID-aware rendering; others use plain bullets.
|
|
178
210
|
const groups = [
|
|
179
|
-
["success_criteria", "Success Criteria", state.successCriteria],
|
|
180
|
-
["active_constraints", "Active Constraints", activeNonHardConstraints(state)],
|
|
181
|
-
["changes", "Changes Since Last Round", state.changesSinceLastRound],
|
|
182
|
-
["remaining", "Remaining", state.remainingCriteria],
|
|
183
|
-
["blockers", "Blockers", state.blockers],
|
|
184
|
-
["discoveries", "Discoveries", state.discoveries],
|
|
185
|
-
["rolling_outcomes", "Cross-Round Outcomes", state.rollingOutcomes],
|
|
186
|
-
["recurring_issues", "Recurring Issues", state.recurringIssues],
|
|
187
|
-
["failed_patterns", "Failed Patterns", state.failedPatterns],
|
|
211
|
+
["success_criteria", "Success Criteria", state.successCriteria, "cr"],
|
|
212
|
+
["active_constraints", "Active Constraints", activeNonHardConstraints(state), "c"],
|
|
213
|
+
["changes", "Changes Since Last Round", state.changesSinceLastRound, null],
|
|
214
|
+
["remaining", "Remaining", state.remainingCriteria, "cr"],
|
|
215
|
+
["blockers", "Blockers", state.blockers, null],
|
|
216
|
+
["discoveries", "Discoveries", state.discoveries, null],
|
|
217
|
+
["rolling_outcomes", "Cross-Round Outcomes", state.rollingOutcomes, null],
|
|
218
|
+
["recurring_issues", "Recurring Issues", state.recurringIssues, null],
|
|
219
|
+
["failed_patterns", "Failed Patterns", state.failedPatterns, null],
|
|
188
220
|
];
|
|
189
|
-
for (const [id, title, values] of groups) {
|
|
221
|
+
for (const [id, title, values, idPrefix] of groups) {
|
|
190
222
|
if (values.length > 0) {
|
|
191
|
-
|
|
223
|
+
const body = idPrefix
|
|
224
|
+
? bulletsWithIds(values, idPrefix)
|
|
225
|
+
: bullets(values);
|
|
226
|
+
sections.push({ id, text: section(title, body), mandatory: false });
|
|
192
227
|
}
|
|
193
228
|
}
|
|
194
229
|
if (state.milestones.length > 0) {
|
|
@@ -218,7 +253,7 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
218
253
|
// In progress
|
|
219
254
|
const inProgress = state.subGoals.filter((sg) => sg.status === "in_progress");
|
|
220
255
|
for (const sg of inProgress) {
|
|
221
|
-
lines.push(`🔄 ${sg.description} (since R${sg.status_changed_at_round})`);
|
|
256
|
+
lines.push(`🔄 [\`${sg.id}\`] ${sg.description} (since R${sg.status_changed_at_round})`);
|
|
222
257
|
}
|
|
223
258
|
// Pending (by age)
|
|
224
259
|
const pending = state.subGoals.filter((sg) => sg.status === "pending");
|
|
@@ -226,7 +261,7 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
226
261
|
for (const sg of shownPending) {
|
|
227
262
|
const age = state.round - sg.declared_at_round;
|
|
228
263
|
const stale = age >= 10 ? " ⚠️ stale" : "";
|
|
229
|
-
lines.push(`⏳ ${sg.description} (pending ${age} rounds)${stale}`);
|
|
264
|
+
lines.push(`⏳ [\`${sg.id}\`] ${sg.description} (pending ${age} rounds)${stale}`);
|
|
230
265
|
}
|
|
231
266
|
if (pending.length > 10) {
|
|
232
267
|
lines.push(`... and ${pending.length - 10} more pending`);
|
|
@@ -234,13 +269,13 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
234
269
|
// Blocked
|
|
235
270
|
const blocked = state.subGoals.filter((sg) => sg.status === "blocked");
|
|
236
271
|
for (const sg of blocked) {
|
|
237
|
-
lines.push(`🚫 ${sg.description} (blocked since R${sg.status_changed_at_round})`);
|
|
272
|
+
lines.push(`🚫 [\`${sg.id}\`] ${sg.description} (blocked since R${sg.status_changed_at_round})`);
|
|
238
273
|
}
|
|
239
274
|
// Recently done
|
|
240
275
|
const done = state.subGoals.filter((sg) => sg.status === "done");
|
|
241
276
|
const recentDone = done.slice(0, 5);
|
|
242
277
|
for (const sg of recentDone) {
|
|
243
|
-
lines.push(`✅ ${sg.description} (done R${sg.completed_at_round ?? sg.status_changed_at_round})`);
|
|
278
|
+
lines.push(`✅ [\`${sg.id}\`] ${sg.description} (done R${sg.completed_at_round ?? sg.status_changed_at_round})`);
|
|
244
279
|
}
|
|
245
280
|
if (done.length > 5) {
|
|
246
281
|
lines.push(`... and ${done.length - 5} more completed`);
|
|
@@ -278,6 +313,90 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
278
313
|
mandatory: false,
|
|
279
314
|
});
|
|
280
315
|
}
|
|
316
|
+
// ── v2.8: Path B gap fills (previously only in full state markdown) ──
|
|
317
|
+
// Progress Dashboard
|
|
318
|
+
if (state.progress.estimate !== null ||
|
|
319
|
+
state.progress.criteriaMet.length > 0 ||
|
|
320
|
+
state.progress.criteriaRemaining.length > 0 ||
|
|
321
|
+
state.progress.filesChanged.length > 0 ||
|
|
322
|
+
state.progress.tests !== null) {
|
|
323
|
+
const lines = [];
|
|
324
|
+
const total = state.progress.criteriaMet.length + state.progress.criteriaRemaining.length;
|
|
325
|
+
if (total > 0) {
|
|
326
|
+
lines.push(`**Criteria**: ${state.progress.criteriaMet.length}/${total} met`);
|
|
327
|
+
}
|
|
328
|
+
if (state.progress.estimate !== null) {
|
|
329
|
+
lines.push(`**Estimated Completion**: ${(state.progress.estimate * 100).toFixed(0)}%`);
|
|
330
|
+
}
|
|
331
|
+
if (state.progress.tests) {
|
|
332
|
+
lines.push(`**Tests**: ${state.progress.tests.passed} passed, ` +
|
|
333
|
+
`${state.progress.tests.failed} failed, ${state.progress.tests.skipped} skipped`);
|
|
334
|
+
}
|
|
335
|
+
if (state.progress.filesChanged.length > 0) {
|
|
336
|
+
lines.push("**Files Changed**:");
|
|
337
|
+
for (const f of state.progress.filesChanged.slice(0, 10))
|
|
338
|
+
lines.push(`- ${f}`);
|
|
339
|
+
if (state.progress.filesChanged.length > 10) {
|
|
340
|
+
lines.push(`- ... and ${state.progress.filesChanged.length - 10} more`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
sections.push({
|
|
344
|
+
id: "progress",
|
|
345
|
+
text: section("Progress Dashboard", lines.join("\n")),
|
|
346
|
+
mandatory: false,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
// Retired Constraints
|
|
350
|
+
if (state.retiredConstraints.length > 0) {
|
|
351
|
+
const lines = state.retiredConstraints.map((c) => `- ~${c}~ (retired)`);
|
|
352
|
+
sections.push({
|
|
353
|
+
id: "retired_constraints",
|
|
354
|
+
text: section("Retired Constraints", lines.join("\n")),
|
|
355
|
+
mandatory: false,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
// Inactive Constraints (with decay metadata)
|
|
359
|
+
if (state.inactiveConstraints.length > 0) {
|
|
360
|
+
const lines = [
|
|
361
|
+
"> Demoted after prolonged inactivity. Auto-reactivated if violated again.",
|
|
362
|
+
"",
|
|
363
|
+
];
|
|
364
|
+
for (const c of state.inactiveConstraints) {
|
|
365
|
+
const meta = state.constraintMetadata.find((m) => m.text === c);
|
|
366
|
+
const age = meta
|
|
367
|
+
? state.round - (meta.last_violated_at_round || meta.discovered_at_round || 0)
|
|
368
|
+
: "?";
|
|
369
|
+
const lastV = meta?.last_violated_at_round
|
|
370
|
+
? `last violated R${meta.last_violated_at_round}`
|
|
371
|
+
: "never violated";
|
|
372
|
+
lines.push(`- ${c} (${lastV}, ${age} rounds inactive)`);
|
|
373
|
+
}
|
|
374
|
+
sections.push({
|
|
375
|
+
id: "inactive_constraints",
|
|
376
|
+
text: section("Inactive Constraints", lines.join("\n")),
|
|
377
|
+
mandatory: false,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
// Agent Trust Score + Trend
|
|
381
|
+
if (state.agentTrustScore !== undefined) {
|
|
382
|
+
const barLen = 10;
|
|
383
|
+
const filled = Math.round(state.agentTrustScore * barLen);
|
|
384
|
+
const bar = "█".repeat(filled) + "░".repeat(barLen - filled);
|
|
385
|
+
const lines = [
|
|
386
|
+
`${bar} ${(state.agentTrustScore * 100).toFixed(0)}%`,
|
|
387
|
+
];
|
|
388
|
+
if (state.agentTrustTrend.length > 0) {
|
|
389
|
+
const avg = (state.agentTrustTrend.reduce((a, b) => a + b, 0)
|
|
390
|
+
/ state.agentTrustTrend.length).toFixed(2);
|
|
391
|
+
lines.push(`Trend (last ${state.agentTrustTrend.length}): ${state.agentTrustTrend.join(" → ")}`);
|
|
392
|
+
lines.push(`Average: ${avg}`);
|
|
393
|
+
}
|
|
394
|
+
sections.push({
|
|
395
|
+
id: "agent_trust",
|
|
396
|
+
text: section("Agent Trust", lines.join("\n")),
|
|
397
|
+
mandatory: false,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
281
400
|
return sections;
|
|
282
401
|
}
|
|
283
402
|
function selectSections(input) {
|
|
@@ -305,20 +424,257 @@ function renderWithinBudget(sections, fixedText, budget) {
|
|
|
305
424
|
}
|
|
306
425
|
return { rendered, included };
|
|
307
426
|
}
|
|
427
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
428
|
+
// v2.9: Prompt Requests — model-expressed information needs
|
|
429
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
430
|
+
/** Match a free-text emphasis target against state items.
|
|
431
|
+
* v2.11: ID-first matching. When a target is an ID (c-/cr-/sg-XXXXXXXX),
|
|
432
|
+
* candidates that derive to the same ID get score 1.0. Falls back to
|
|
433
|
+
* Jaccard similarity otherwise. */
|
|
434
|
+
function matchEmphasize(targets, candidates, cap) {
|
|
435
|
+
if (targets.length === 0 || candidates.length === 0)
|
|
436
|
+
return [];
|
|
437
|
+
const idRegex = /^(c|cr|sg)-[a-f0-9]{8}$/;
|
|
438
|
+
const scored = candidates.map((c) => {
|
|
439
|
+
let best = 0;
|
|
440
|
+
for (const t of targets) {
|
|
441
|
+
let s = 0;
|
|
442
|
+
// Phase 1: exact ID match (v2.11)
|
|
443
|
+
const idMatch = t.match(idRegex);
|
|
444
|
+
if (idMatch) {
|
|
445
|
+
const candidateId = idMatch[1] + "-" + deriveItemId(c);
|
|
446
|
+
if (candidateId === t)
|
|
447
|
+
s = 1.0;
|
|
448
|
+
}
|
|
449
|
+
// Phase 2: Jaccard fallback
|
|
450
|
+
if (s === 0)
|
|
451
|
+
s = jaccardSimilarity(t, c);
|
|
452
|
+
if (s > best)
|
|
453
|
+
best = s;
|
|
454
|
+
}
|
|
455
|
+
return { text: c, score: best };
|
|
456
|
+
});
|
|
457
|
+
return scored
|
|
458
|
+
.filter((e) => e.score >= getPolicy().evolution.constraint_match_threshold)
|
|
459
|
+
.sort((a, b) => b.score - a.score)
|
|
460
|
+
.slice(0, cap)
|
|
461
|
+
.map((e) => e.text);
|
|
462
|
+
}
|
|
463
|
+
/** Render the "Confusion Alerts" section that appears at the prompt top.
|
|
464
|
+
* Compiler auto-matches each confusion point against state sections. */
|
|
465
|
+
function renderConfusionAlerts(points, state, expandAll) {
|
|
466
|
+
if (points.length === 0)
|
|
467
|
+
return "";
|
|
468
|
+
const lines = [
|
|
469
|
+
"## ⚠️ Confusion Alerts",
|
|
470
|
+
"",
|
|
471
|
+
"You flagged these as confusing in the previous round.",
|
|
472
|
+
"Address each before proceeding — the state file has full context.",
|
|
473
|
+
"",
|
|
474
|
+
];
|
|
475
|
+
const sections = [
|
|
476
|
+
{ keyword: "milestone", label: "Phase History", ref: "## Phase History" },
|
|
477
|
+
{ keyword: "sub-goal", label: "Sub-Goal Dashboard", ref: "## Sub-Goal Dashboard" },
|
|
478
|
+
{ keyword: "constraint", label: "Active Constraints / Constraint Lifecycle", ref: "## Active Constraints" },
|
|
479
|
+
{ keyword: "trust", label: "Agent Trust", ref: "## Agent Trust" },
|
|
480
|
+
{ keyword: "progress", label: "Progress Dashboard", ref: "## Progress Dashboard" },
|
|
481
|
+
{ keyword: "criteria", label: "Success Criteria", ref: "## Success Criteria" },
|
|
482
|
+
{ keyword: "phase", label: "Phase History", ref: "## Phase History" },
|
|
483
|
+
{ keyword: "objective", label: "Loop Objective", ref: "## Loop Objective" },
|
|
484
|
+
{ keyword: "loop", label: "Loop Summary", ref: "## Loop Summary" },
|
|
485
|
+
];
|
|
486
|
+
const shown = expandAll ? points : points.slice(0, 1);
|
|
487
|
+
for (const point of shown) {
|
|
488
|
+
const truncated = point.length > 200 ? point.slice(0, 197) + "…" : point;
|
|
489
|
+
// Find best-matching state section
|
|
490
|
+
let bestSection = null;
|
|
491
|
+
let bestScore = 0;
|
|
492
|
+
for (const sec of sections) {
|
|
493
|
+
const score = jaccardSimilarity(truncated.toLowerCase(), sec.keyword);
|
|
494
|
+
if (score > bestScore) {
|
|
495
|
+
bestScore = score;
|
|
496
|
+
bestSection = sec;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const pointer = bestSection && bestScore >= 0.15
|
|
500
|
+
? `→ Check **${bestSection.label}** (\`${bestSection.ref}\`) in the state file.`
|
|
501
|
+
: "→ Read the full state file for relevant context.";
|
|
502
|
+
lines.push(`- **"${truncated}"**`);
|
|
503
|
+
lines.push(` ${pointer}`);
|
|
504
|
+
lines.push("");
|
|
505
|
+
}
|
|
506
|
+
if (!expandAll && points.length > 1) {
|
|
507
|
+
lines.push(`*… and ${points.length - 1} more confusion point(s) — see state file.*`);
|
|
508
|
+
lines.push("");
|
|
509
|
+
}
|
|
510
|
+
return lines.join("\n");
|
|
511
|
+
}
|
|
512
|
+
/** Render the "Critical Context" section — items the model emphasized.
|
|
513
|
+
* Pulled from active constraints, discoveries, and blocking issues. */
|
|
514
|
+
function renderCriticalContext(emphasized, state) {
|
|
515
|
+
if (emphasized.length === 0)
|
|
516
|
+
return "";
|
|
517
|
+
const lines = [
|
|
518
|
+
"## 🔴 Critical Context",
|
|
519
|
+
"",
|
|
520
|
+
"You flagged these as important for this round. Prioritize them.",
|
|
521
|
+
"",
|
|
522
|
+
];
|
|
523
|
+
for (const item of emphasized) {
|
|
524
|
+
lines.push(`- ${item}`);
|
|
525
|
+
}
|
|
526
|
+
lines.push("");
|
|
527
|
+
return lines.join("\n");
|
|
528
|
+
}
|
|
529
|
+
/** Expand a specific section to L2-level detail within a L1 prompt.
|
|
530
|
+
* Returns the expanded section text, or empty string if the section
|
|
531
|
+
* has no content to expand. */
|
|
532
|
+
function renderExpandedSection(sectionName, state) {
|
|
533
|
+
switch (sectionName) {
|
|
534
|
+
case "milestones": {
|
|
535
|
+
if (state.milestones.length === 0)
|
|
536
|
+
return "";
|
|
537
|
+
const text = state.milestones
|
|
538
|
+
.map((m) => {
|
|
539
|
+
const icon = m.kind === "agent_declared" ? "🏁" : m.kind === "criteria_milestone" ? "✅" : "📍";
|
|
540
|
+
const lines = [
|
|
541
|
+
`**${icon} ${m.label}** (R${m.round_range.start}–R${m.round_range.end}, ${(m.progress_at_boundary * 100).toFixed(0)}%)`,
|
|
542
|
+
` Outcome: ${m.outcome}`,
|
|
543
|
+
];
|
|
544
|
+
if (m.carried_constraints.length > 0)
|
|
545
|
+
lines.push(` Carried: ${m.carried_constraints.slice(0, 3).join("; ")}`);
|
|
546
|
+
if (m.resolved_constraints.length > 0)
|
|
547
|
+
lines.push(` Resolved: ${m.resolved_constraints.slice(0, 3).join("; ")}`);
|
|
548
|
+
return lines.join("\n");
|
|
549
|
+
})
|
|
550
|
+
.join("\n\n");
|
|
551
|
+
return section("Expanded Phase History", text);
|
|
552
|
+
}
|
|
553
|
+
case "sub_goals": {
|
|
554
|
+
if (state.subGoals.length === 0)
|
|
555
|
+
return "";
|
|
556
|
+
const lines = [];
|
|
557
|
+
for (const sg of state.subGoals) {
|
|
558
|
+
const icon = sg.status === "in_progress" ? "🔄" :
|
|
559
|
+
sg.status === "done" ? "✅" :
|
|
560
|
+
sg.status === "blocked" ? "🚫" :
|
|
561
|
+
sg.status === "canceled" ? "❌" : "⏳";
|
|
562
|
+
const stale = sg.status === "pending" && state.round - sg.declared_at_round >= 10 ? " ⚠️ stale" : "";
|
|
563
|
+
const detail = sg.status === "done" && sg.completed_at_round
|
|
564
|
+
? ` (done, R${sg.completed_at_round})`
|
|
565
|
+
: sg.status === "in_progress" ? ` (since R${sg.status_changed_at_round})`
|
|
566
|
+
: sg.status === "pending" ? ` (since R${sg.declared_at_round})`
|
|
567
|
+
: sg.status === "blocked" ? ` (blocked R${sg.status_changed_at_round})`
|
|
568
|
+
: ` (canceled R${sg.status_changed_at_round})`;
|
|
569
|
+
lines.push(`- ${icon} [\`${sg.id}\`] ${sg.description}${detail}${stale}`);
|
|
570
|
+
}
|
|
571
|
+
return section("Expanded Sub-Goal Dashboard", lines.join("\n"));
|
|
572
|
+
}
|
|
573
|
+
case "constraint_lifecycle": {
|
|
574
|
+
if (state.inactiveConstraints.length === 0 && state.retiredConstraints.length === 0)
|
|
575
|
+
return "";
|
|
576
|
+
const lines = [];
|
|
577
|
+
if (state.inactiveConstraints.length > 0) {
|
|
578
|
+
lines.push("**Inactive (auto-decayed):**");
|
|
579
|
+
for (const c of state.inactiveConstraints) {
|
|
580
|
+
const meta = state.constraintMetadata.find((m) => m.text === c);
|
|
581
|
+
const age = meta ? state.round - (meta.last_violated_at_round || meta.discovered_at_round || 0) : "?";
|
|
582
|
+
lines.push(`- ${c} (${age} rounds inactive)`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (state.retiredConstraints.length > 0) {
|
|
586
|
+
lines.push("**Retired:**");
|
|
587
|
+
for (const c of state.retiredConstraints)
|
|
588
|
+
lines.push(`- ~${c}~`);
|
|
589
|
+
}
|
|
590
|
+
return section("Constraint Lifecycle", lines.join("\n"));
|
|
591
|
+
}
|
|
592
|
+
case "agent_trust": {
|
|
593
|
+
if (state.agentTrustScore === undefined)
|
|
594
|
+
return "";
|
|
595
|
+
const barLen = 10;
|
|
596
|
+
const filled = Math.round(state.agentTrustScore * barLen);
|
|
597
|
+
const bar = "█".repeat(filled) + "░".repeat(barLen - filled);
|
|
598
|
+
const lines = [
|
|
599
|
+
`${bar} ${(state.agentTrustScore * 100).toFixed(0)}%`,
|
|
600
|
+
];
|
|
601
|
+
if (state.agentTrustTrend.length > 0) {
|
|
602
|
+
lines.push(`Trend: ${state.agentTrustTrend.join(" → ")}`);
|
|
603
|
+
}
|
|
604
|
+
return section("Agent Trust (Expanded)", lines.join("\n"));
|
|
605
|
+
}
|
|
606
|
+
case "progress": {
|
|
607
|
+
const p = state.progress;
|
|
608
|
+
if (p.estimate === null && p.criteriaMet.length === 0 && p.criteriaRemaining.length === 0)
|
|
609
|
+
return "";
|
|
610
|
+
const lines = [];
|
|
611
|
+
const total = p.criteriaMet.length + p.criteriaRemaining.length;
|
|
612
|
+
if (total > 0)
|
|
613
|
+
lines.push(`**Criteria**: ${p.criteriaMet.length}/${total} met`);
|
|
614
|
+
if (p.estimate !== null)
|
|
615
|
+
lines.push(`**Estimate**: ${(p.estimate * 100).toFixed(0)}%`);
|
|
616
|
+
if (p.tests)
|
|
617
|
+
lines.push(`**Tests**: ${p.tests.passed} passed, ${p.tests.failed} failed, ${p.tests.skipped} skipped`);
|
|
618
|
+
if (p.filesChanged.length > 0) {
|
|
619
|
+
lines.push("**Files Changed**:");
|
|
620
|
+
for (const f of p.filesChanged.slice(0, 10))
|
|
621
|
+
lines.push(`- ${f}`);
|
|
622
|
+
}
|
|
623
|
+
return section("Progress Dashboard (Expanded)", lines.join("\n"));
|
|
624
|
+
}
|
|
625
|
+
case "loop_synthesis": {
|
|
626
|
+
if (!state.loopSynthesis)
|
|
627
|
+
return "";
|
|
628
|
+
return section("Loop Summary", state.loopSynthesis);
|
|
629
|
+
}
|
|
630
|
+
default:
|
|
631
|
+
return "";
|
|
632
|
+
}
|
|
633
|
+
}
|
|
308
634
|
export function assemblePromptArtifact(input) {
|
|
309
635
|
const mode = input.mode ?? "adaptive";
|
|
310
636
|
const budgets = { ...DEFAULT_PROMPT_BUDGETS, ...input.budgets };
|
|
311
637
|
const budget = budgets[input.level];
|
|
312
638
|
const stateHash = hashCanonicalState(input.state);
|
|
313
639
|
const attempt = Math.max(1, input.attempt ?? 1);
|
|
640
|
+
// ── State file reference + conditional read instruction ────────────
|
|
641
|
+
// L2 rounds (full rehydration), recovery, rejection, drift, and pointer
|
|
642
|
+
// mode all benefit from the model reading the durable state file rather
|
|
643
|
+
// than relying on potentially-compacted conversation history.
|
|
644
|
+
const fullContextReasons = new Set([
|
|
645
|
+
"first_round", "plan_boundary", "checkpoint_boundary", "goal_changed",
|
|
646
|
+
"missing_previous_state", "verification_contradicted",
|
|
647
|
+
"rejection_rehydrate", "recovery_boundary", "state_drift",
|
|
648
|
+
"periodic_refresh",
|
|
649
|
+
]);
|
|
650
|
+
const needsFullContext = input.level === "l2"
|
|
651
|
+
|| input.mode === "pointer"
|
|
652
|
+
|| input.reasons.some((r) => fullContextReasons.has(r));
|
|
653
|
+
const stateFilePath = input.state.stateFilePath;
|
|
654
|
+
const stateFileRef = stateFilePath
|
|
655
|
+
? `📄 Full state: \`${stateFilePath}\`\n`
|
|
656
|
+
: "";
|
|
657
|
+
const readInstruction = needsFullContext && stateFilePath
|
|
658
|
+
? [
|
|
659
|
+
"",
|
|
660
|
+
"> ⚠️ **Read the full state file before acting.** The summary below is a",
|
|
661
|
+
"> capsule. The durable state file contains complete phase history,",
|
|
662
|
+
"> sub-goal dashboard, constraint lifecycle, and agent trust data.",
|
|
663
|
+
"> Conversation history may be incomplete after compaction or session",
|
|
664
|
+
"> resume — the state file is the source of truth.",
|
|
665
|
+
"",
|
|
666
|
+
].join("\n")
|
|
667
|
+
: "";
|
|
314
668
|
const header = [
|
|
315
669
|
`## LoopForge Round ${input.state.round}`,
|
|
316
670
|
`State: ${stateHash.slice(0, 12)} | Level: ${input.level.toUpperCase()} | Attempt: ${attempt}`,
|
|
671
|
+
stateFileRef,
|
|
672
|
+
readInstruction,
|
|
317
673
|
"",
|
|
318
674
|
].join("\n");
|
|
319
675
|
const sections = selectSections(input);
|
|
320
|
-
const pointer =
|
|
321
|
-
? section("Full State",
|
|
676
|
+
const pointer = stateFilePath
|
|
677
|
+
? section("Full State", stateFilePath)
|
|
322
678
|
: "";
|
|
323
679
|
const evaluation = input.selfEvaluationBlock.trim()
|
|
324
680
|
? `${input.selfEvaluationBlock.trim()}\n`
|
|
@@ -351,13 +707,55 @@ export function assemblePromptArtifact(input) {
|
|
|
351
707
|
evaluation,
|
|
352
708
|
].join("\n");
|
|
353
709
|
const footer = [pointer, howToRespond].join("");
|
|
354
|
-
|
|
710
|
+
// ── v2.9: Prompt Requests — model-expressed information needs ──────────
|
|
711
|
+
const pr = input.promptRequests;
|
|
712
|
+
const levelIsL0 = input.level === "l0";
|
|
713
|
+
const levelIsL1 = input.level === "l1";
|
|
714
|
+
const levelIsL2 = input.level === "l2";
|
|
715
|
+
// Confusion alerts: L1/L2 only, rendered at the top (before mandatory sections)
|
|
716
|
+
const confusionText = (!levelIsL0 && pr?.confusion_points?.length)
|
|
717
|
+
? renderConfusionAlerts(pr.confusion_points, input.state, levelIsL2)
|
|
718
|
+
: "";
|
|
719
|
+
// Emphasize: L1/L2 only, matched against active state items
|
|
720
|
+
const policy = getPolicy().prompt;
|
|
721
|
+
const emphasizeCap = levelIsL2 ? policy.max_emphasize_l2 : policy.max_emphasize_l1;
|
|
722
|
+
const emphasized = (!levelIsL0 && pr?.emphasize?.length)
|
|
723
|
+
? matchEmphasize(pr.emphasize, [
|
|
724
|
+
...input.state.activeConstraints,
|
|
725
|
+
...input.state.discoveries,
|
|
726
|
+
...input.state.blockers,
|
|
727
|
+
...input.state.changesSinceLastRound,
|
|
728
|
+
], emphasizeCap)
|
|
729
|
+
: [];
|
|
730
|
+
const criticalContextText = renderCriticalContext(emphasized, input.state);
|
|
731
|
+
// Expand: L1 only, max 1 section (L2 already expanded, L0 ignored)
|
|
732
|
+
const expandCap = levelIsL1 ? policy.max_expand_l1 : 0;
|
|
733
|
+
const expandedSection = (pr?.expand?.length && expandCap > 0)
|
|
734
|
+
? renderExpandedSection(pr.expand[0], input.state)
|
|
735
|
+
: "";
|
|
736
|
+
// Inject confusion alerts into the header area (before mandatory sections)
|
|
737
|
+
const fixedText = header + confusionText;
|
|
738
|
+
// Inject critical context as a mandatory section
|
|
739
|
+
let augmentedSections = sections;
|
|
740
|
+
if (criticalContextText) {
|
|
741
|
+
augmentedSections = [
|
|
742
|
+
{
|
|
743
|
+
id: "critical_context",
|
|
744
|
+
text: criticalContextText,
|
|
745
|
+
mandatory: true,
|
|
746
|
+
},
|
|
747
|
+
...sections,
|
|
748
|
+
];
|
|
749
|
+
}
|
|
355
750
|
const selected = mode === "pointer"
|
|
356
|
-
? renderWithinBudget(
|
|
357
|
-
: renderWithinBudget(
|
|
358
|
-
const renderedPrompt = selected.rendered + footer;
|
|
751
|
+
? renderWithinBudget(augmentedSections.filter((item) => item.mandatory), fixedText, budget)
|
|
752
|
+
: renderWithinBudget(augmentedSections, fixedText, budget - footer.length - expandedSection.length);
|
|
753
|
+
const renderedPrompt = selected.rendered + expandedSection + footer;
|
|
359
754
|
const includedSections = [
|
|
360
755
|
...selected.included,
|
|
756
|
+
...(confusionText ? ["confusion_alerts"] : []),
|
|
757
|
+
...(criticalContextText ? ["critical_context"] : []),
|
|
758
|
+
...(expandedSection ? [`expand:${pr?.expand?.[0] ?? ""}`] : []),
|
|
361
759
|
...(pointer ? ["state_pointer"] : []),
|
|
362
760
|
...(evaluation ? ["self_evaluation"] : []),
|
|
363
761
|
];
|