loopforge 2.0.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/backends/interface.d.ts +4 -3
- package/dist/backends/interface.d.ts.map +1 -1
- package/dist/backends/interface.js +4 -1
- package/dist/backends/interface.js.map +1 -1
- package/dist/canonical-state.d.ts +23 -2
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +90 -6
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -2
- package/dist/cli.js.map +1 -1
- package/dist/enforcement-gate.d.ts +1 -1
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +86 -7
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +3 -33
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +127 -343
- package/dist/engine.js.map +1 -1
- package/dist/evidence-provider.d.ts +38 -5
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +132 -46
- package/dist/evidence-provider.js.map +1 -1
- package/dist/index.d.ts +10 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -9
- package/dist/index.js.map +1 -1
- package/dist/interop.d.ts +5 -0
- package/dist/interop.d.ts.map +1 -1
- package/dist/interop.js +5 -0
- package/dist/interop.js.map +1 -1
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +515 -50
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +82 -0
- package/dist/loop-extras-parser.d.ts.map +1 -0
- package/dist/loop-extras-parser.js +217 -0
- package/dist/loop-extras-parser.js.map +1 -0
- package/dist/loop-store.d.ts +26 -4
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +118 -8
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/session.d.ts +29 -6
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +187 -161
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +33 -5
- package/dist/mcp/tools.js.map +1 -1
- package/dist/observability.d.ts +5 -36
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +9 -79
- package/dist/observability.js.map +1 -1
- package/dist/policy-metrics.d.ts +1 -0
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +2 -0
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +58 -8
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +25 -12
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +129 -4
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/protocol.d.ts +118 -87
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +40 -30
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +0 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +1 -87
- package/dist/replay.js.map +1 -1
- package/dist/round-coordinator.d.ts +4 -6
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +75 -80
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +1 -0
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +2 -0
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +2 -22
- package/dist/round-transaction.js.map +1 -1
- package/dist/run.d.ts +51 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +200 -0
- package/dist/run.js.map +1 -0
- package/dist/self-eval.d.ts +23 -0
- package/dist/self-eval.d.ts.map +1 -0
- package/dist/self-eval.js +151 -0
- package/dist/self-eval.js.map +1 -0
- package/dist/storage.d.ts +3 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +70 -68
- package/dist/storage.js.map +1 -1
- package/dist/token-utils.d.ts +22 -0
- package/dist/token-utils.d.ts.map +1 -0
- package/dist/token-utils.js +46 -0
- package/dist/token-utils.js.map +1 -0
- package/dist/verification-gate.d.ts +17 -23
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +241 -135
- package/dist/verification-gate.js.map +1 -1
- package/loop_policy.json +21 -12
- package/package.json +2 -2
- package/dist/runtime.d.ts +0 -94
- package/dist/runtime.d.ts.map +0 -1
- package/dist/runtime.js +0 -718
- package/dist/runtime.js.map +0 -1
package/dist/mcp/session.js
CHANGED
|
@@ -5,22 +5,20 @@
|
|
|
5
5
|
* the advance() cycle: extract → feedback → check stop → compile next.
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { LoopForgeEngine, extractSelfEvaluation
|
|
8
|
+
import { LoopForgeEngine, extractSelfEvaluation } from "../engine.js";
|
|
9
9
|
import { checkLoopHealth } from "../loop-compiler.js";
|
|
10
10
|
import { getPolicy } from "../policy.js";
|
|
11
|
-
import { Mode, makeLoopCompileRequest } from "../protocol.js";
|
|
11
|
+
import { Mode, makeLoopCompileRequest, makeLoopRoundResult } from "../protocol.js";
|
|
12
12
|
import { ReplayBackend } from "../replay.js";
|
|
13
|
-
import { FileLoopStore, LoopStoreBackend } from "../loop-store.js";
|
|
13
|
+
import { FileLoopStore, LoopStoreBackend, VaultBackendLoopStore } from "../loop-store.js";
|
|
14
14
|
import { EvidenceCollector } from "../evidence-provider.js";
|
|
15
15
|
import { parseRoundTransactionSnapshot, prepareRoundTransaction, } from "../round-transaction.js";
|
|
16
16
|
import { RoundDriver } from "../round-driver.js";
|
|
17
17
|
import { logEvent } from "../observability.js";
|
|
18
18
|
import { policyMetrics } from "../policy-metrics.js";
|
|
19
19
|
import { SessionLeaseConflictError, VaultSessionStateStore, } from "../storage.js";
|
|
20
|
-
import { createCognitiveCheckpoint } from "../interop.js";
|
|
21
20
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
22
|
-
function buildLoopRequest(session, lastEval,
|
|
23
|
-
verificationFlags) {
|
|
21
|
+
function buildLoopRequest(session, lastEval, verificationFlags) {
|
|
24
22
|
const req = {
|
|
25
23
|
task: session.task,
|
|
26
24
|
mode: Mode.LOOP_COMPILE,
|
|
@@ -33,7 +31,7 @@ verificationFlags) {
|
|
|
33
31
|
verification_flags: verificationFlags ?? [],
|
|
34
32
|
};
|
|
35
33
|
if (lastEval) {
|
|
36
|
-
req.last_round_result = {
|
|
34
|
+
req.last_round_result = makeLoopRoundResult({
|
|
37
35
|
round: session.currentRound - 1,
|
|
38
36
|
success: lastEval.success,
|
|
39
37
|
output_summary: lastEval.output_summary,
|
|
@@ -62,24 +60,14 @@ verificationFlags) {
|
|
|
62
60
|
checkpoint_label: lastEval.checkpoint_label ?? "",
|
|
63
61
|
// v1.16: Agent's declared next action
|
|
64
62
|
next_action: lastEval.next_action,
|
|
65
|
-
|
|
63
|
+
// v2.2: Sub-goal lifecycle
|
|
64
|
+
completed_subtasks: lastEval.completed_subtasks ?? [],
|
|
65
|
+
blocked_subtasks: lastEval.blocked_subtasks ?? [],
|
|
66
|
+
canceled_subtasks: lastEval.canceled_subtasks ?? [],
|
|
67
|
+
});
|
|
66
68
|
}
|
|
67
69
|
return req;
|
|
68
70
|
}
|
|
69
|
-
function parseWarnings(prompt) {
|
|
70
|
-
if (!prompt)
|
|
71
|
-
return [];
|
|
72
|
-
const warnings = [];
|
|
73
|
-
const warnSection = prompt.match(/### Warnings\n([\s\S]*?)(?=\n###|\n\*\*|$)/);
|
|
74
|
-
if (warnSection) {
|
|
75
|
-
for (const line of warnSection[1].split("\n")) {
|
|
76
|
-
const m = line.match(/- ⚠️\s*(.+)/);
|
|
77
|
-
if (m)
|
|
78
|
-
warnings.push(m[1]);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return warnings;
|
|
82
|
-
}
|
|
83
71
|
// ── SessionManager ─────────────────────────────────────────────────────────
|
|
84
72
|
export class SessionManager {
|
|
85
73
|
sessions = new Map();
|
|
@@ -91,18 +79,20 @@ export class SessionManager {
|
|
|
91
79
|
leaseMs;
|
|
92
80
|
leaseRenewIntervalMs;
|
|
93
81
|
leaseTimer = null;
|
|
94
|
-
checkpointSinks = new Set();
|
|
95
82
|
/** Explicit context provider; never auto-discovered. */
|
|
96
83
|
contextProvider;
|
|
97
84
|
terminalSinks = new Set();
|
|
98
85
|
constructor(storeOrBackend, sessionStore) {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this.
|
|
86
|
+
const isLoopStore = (v) => "readSession" in v && typeof v.readSession === "function";
|
|
87
|
+
const store = storeOrBackend
|
|
88
|
+
? isLoopStore(storeOrBackend)
|
|
89
|
+
? storeOrBackend
|
|
90
|
+
: new VaultBackendLoopStore(storeOrBackend)
|
|
91
|
+
: new FileLoopStore(getPolicy().backend.root_dir);
|
|
92
|
+
this.backend = storeOrBackend && !isLoopStore(storeOrBackend)
|
|
93
|
+
? storeOrBackend
|
|
94
|
+
: new LoopStoreBackend(store);
|
|
95
|
+
this.sessionStore = sessionStore ?? new VaultSessionStateStore(store);
|
|
106
96
|
const mcpPolicy = getPolicy().mcp;
|
|
107
97
|
this.leaseMs = Math.max(1, mcpPolicy.session_lease_ms);
|
|
108
98
|
this.leaseRenewIntervalMs = Math.max(1, Math.min(mcpPolicy.session_lease_renew_interval_ms, this.leaseMs));
|
|
@@ -115,11 +105,6 @@ export class SessionManager {
|
|
|
115
105
|
getOwnerId() {
|
|
116
106
|
return this.ownerId;
|
|
117
107
|
}
|
|
118
|
-
/** Subscribe an external checkpointer; sink failures are isolated. */
|
|
119
|
-
addCheckpointSink(sink) {
|
|
120
|
-
this.checkpointSinks.add(sink);
|
|
121
|
-
return () => this.checkpointSinks.delete(sink);
|
|
122
|
-
}
|
|
123
108
|
addTerminalSink(sink) {
|
|
124
109
|
this.terminalSinks.add(sink);
|
|
125
110
|
return () => this.terminalSinks.delete(sink);
|
|
@@ -165,6 +150,7 @@ export class SessionManager {
|
|
|
165
150
|
round: typeof lineage.current_round === "number" ? lineage.current_round : 0,
|
|
166
151
|
prompt: null,
|
|
167
152
|
stopReason: `session_owned_elsewhere:${loopId}`,
|
|
153
|
+
stopDetail: `Another process (PID ${entry?.loop_lineage ? entry.loop_lineage.lease_owner ?? "unknown" : "unknown"}) holds the lease for loop "${loopId}". Wait for the lease to expire or stop the other process.`,
|
|
168
154
|
};
|
|
169
155
|
}
|
|
170
156
|
async withSessionQueue(sessionId, work) {
|
|
@@ -190,7 +176,7 @@ export class SessionManager {
|
|
|
190
176
|
const sessionId = randomUUID();
|
|
191
177
|
const loopId = input.loopId ?? randomUUID();
|
|
192
178
|
const engine = new LoopForgeEngine(this.backend);
|
|
193
|
-
const maxRounds = input.maxRounds ?? getPolicy().
|
|
179
|
+
const maxRounds = input.maxRounds ?? getPolicy().engine.max_rounds;
|
|
194
180
|
// Populate extra fields for the first round
|
|
195
181
|
const request = buildLoopRequest({
|
|
196
182
|
sessionId, loopId, task: input.task, engine, currentRound: 1,
|
|
@@ -240,6 +226,7 @@ export class SessionManager {
|
|
|
240
226
|
roundSnapshot: prepared?.snapshot ?? prepareRoundTransaction(loopId, 1, evidenceBaseline),
|
|
241
227
|
currentPrompt: initialPrompt,
|
|
242
228
|
currentLevel: initialLevel,
|
|
229
|
+
currentWarnings: prepared?.warnings ?? [],
|
|
243
230
|
};
|
|
244
231
|
this.sessions.set(sessionId, session);
|
|
245
232
|
policyMetrics.recordStrategy(loopId, initialLevel);
|
|
@@ -258,8 +245,7 @@ export class SessionManager {
|
|
|
258
245
|
prompt: initialPrompt,
|
|
259
246
|
level: initialLevel,
|
|
260
247
|
roundSuccess: false,
|
|
261
|
-
|
|
262
|
-
warnings: parseWarnings(initialPrompt),
|
|
248
|
+
warnings: prepared?.warnings ?? [],
|
|
263
249
|
};
|
|
264
250
|
}
|
|
265
251
|
get(sessionId) {
|
|
@@ -290,7 +276,7 @@ export class SessionManager {
|
|
|
290
276
|
if (!session)
|
|
291
277
|
return false;
|
|
292
278
|
session.status = "stopped";
|
|
293
|
-
this.save(session);
|
|
279
|
+
this.save(session);
|
|
294
280
|
void this.notifyTerminal(session, "cancelled");
|
|
295
281
|
this.sessions.delete(sessionId);
|
|
296
282
|
logEvent("session_end", {
|
|
@@ -329,9 +315,8 @@ export class SessionManager {
|
|
|
329
315
|
roundId: session.roundSnapshot?.roundId,
|
|
330
316
|
prompt: session.currentPrompt,
|
|
331
317
|
level: session.currentLevel ?? "l2",
|
|
332
|
-
quality: 0,
|
|
333
318
|
roundSuccess: undefined,
|
|
334
|
-
warnings:
|
|
319
|
+
warnings: (session.currentWarnings ?? []),
|
|
335
320
|
};
|
|
336
321
|
}
|
|
337
322
|
/** Reconcile the crash window where feedback committed but session_state
|
|
@@ -377,14 +362,20 @@ export class SessionManager {
|
|
|
377
362
|
roundId: recovered.snapshot.roundId,
|
|
378
363
|
prompt: null,
|
|
379
364
|
stopReason: reason,
|
|
365
|
+
stopDetail: reason === "completed"
|
|
366
|
+
? "Task completed successfully — the agent declared should_continue=false with success=true."
|
|
367
|
+
: reason === "blocked"
|
|
368
|
+
? "Agent cannot proceed under current constraints. The task may need revised constraints or human intervention."
|
|
369
|
+
: reason === "failed"
|
|
370
|
+
? "Agent declared should_continue=false with success=false — the task was not achieved."
|
|
371
|
+
: `Loop stopped: ${reason}.`,
|
|
380
372
|
roundSuccess: pr.roundSuccess,
|
|
381
|
-
quality: pr.roundSuccess ? 5 : 1,
|
|
382
373
|
};
|
|
383
374
|
}
|
|
384
375
|
if (pr.action !== "continue")
|
|
385
376
|
return null;
|
|
386
377
|
session.currentRound++;
|
|
387
|
-
const request = buildLoopRequest(session, pr.newLastSelfEval, pr.
|
|
378
|
+
const request = buildLoopRequest(session, pr.newLastSelfEval, pr.verificationFlags);
|
|
388
379
|
const prepared = new RoundDriver(session.engine, this.backend).prepareSync(request, session.loopId, session.currentRound);
|
|
389
380
|
if (!prepared) {
|
|
390
381
|
session.status = "stalled";
|
|
@@ -394,6 +385,7 @@ export class SessionManager {
|
|
|
394
385
|
round: session.currentRound,
|
|
395
386
|
prompt: null,
|
|
396
387
|
stopReason: "stalled",
|
|
388
|
+
stopDetail: "RoundDriver.prepareSync returned null — prompt compilation failed during crash recovery. The session state may be inconsistent.",
|
|
397
389
|
};
|
|
398
390
|
}
|
|
399
391
|
const prompt = prepared.prompt;
|
|
@@ -401,6 +393,7 @@ export class SessionManager {
|
|
|
401
393
|
session.roundSnapshot = prepared.snapshot;
|
|
402
394
|
session.currentPrompt = prompt;
|
|
403
395
|
session.currentLevel = prepared.level;
|
|
396
|
+
session.currentWarnings = prepared?.warnings ?? [];
|
|
404
397
|
policyMetrics.recordStrategy(session.loopId, session.currentLevel);
|
|
405
398
|
this.save(session);
|
|
406
399
|
return {
|
|
@@ -410,8 +403,7 @@ export class SessionManager {
|
|
|
410
403
|
prompt,
|
|
411
404
|
level: session.currentLevel,
|
|
412
405
|
roundSuccess: pr.roundSuccess,
|
|
413
|
-
|
|
414
|
-
warnings: parseWarnings(prompt),
|
|
406
|
+
warnings: (session.currentWarnings ?? []),
|
|
415
407
|
};
|
|
416
408
|
}
|
|
417
409
|
/** v1.18: Resume a paused session. Reconstructs from vault state and
|
|
@@ -490,7 +482,7 @@ export class SessionManager {
|
|
|
490
482
|
session.status = "stopped";
|
|
491
483
|
this.save(session);
|
|
492
484
|
void this.notifyTerminal(session, "stalled");
|
|
493
|
-
return { sessionId: session.sessionId, round: session.currentRound, prompt: null, stopReason: "stalled" };
|
|
485
|
+
return { sessionId: session.sessionId, round: session.currentRound, prompt: null, stopReason: "stalled", stopDetail: "RoundDriver.prepare returned null during unpause — the compiler could not produce a prompt." };
|
|
494
486
|
}
|
|
495
487
|
const prompt = prepared.prompt;
|
|
496
488
|
const level = prepared.level;
|
|
@@ -498,6 +490,7 @@ export class SessionManager {
|
|
|
498
490
|
session.roundSnapshot = prepared.snapshot;
|
|
499
491
|
session.currentPrompt = prompt;
|
|
500
492
|
session.currentLevel = level;
|
|
493
|
+
session.currentWarnings = prepared?.warnings ?? [];
|
|
501
494
|
policyMetrics.recordStrategy(session.loopId, level);
|
|
502
495
|
this.save(session);
|
|
503
496
|
return {
|
|
@@ -506,7 +499,6 @@ export class SessionManager {
|
|
|
506
499
|
roundId: session.roundSnapshot.roundId,
|
|
507
500
|
prompt,
|
|
508
501
|
level,
|
|
509
|
-
quality: 0,
|
|
510
502
|
roundSuccess: undefined,
|
|
511
503
|
warnings: [],
|
|
512
504
|
};
|
|
@@ -547,20 +539,6 @@ export class SessionManager {
|
|
|
547
539
|
this.sessionStore.save(sessionEntry, {
|
|
548
540
|
expectedLeaseOwner: this.ownerId,
|
|
549
541
|
});
|
|
550
|
-
if (this.checkpointSinks.size > 0) {
|
|
551
|
-
const checkpoint = createCognitiveCheckpoint(session, sessionEntry.timestamp);
|
|
552
|
-
for (const sink of this.checkpointSinks) {
|
|
553
|
-
try {
|
|
554
|
-
const pending = sink.save(checkpoint);
|
|
555
|
-
if (pending && typeof pending.then === "function") {
|
|
556
|
-
void pending.catch(() => undefined);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
catch {
|
|
560
|
-
// External adapters must not affect session durability.
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
542
|
}
|
|
565
543
|
/** Reconstruct a McpSession from a vault session_state entry.
|
|
566
544
|
* Returns null if the entry is not "running" status.
|
|
@@ -575,7 +553,7 @@ export class SessionManager {
|
|
|
575
553
|
const currentRound = lineage.current_round ?? 1;
|
|
576
554
|
const successTrajectory = lineage.success_trajectory ?? lineage.quality_trajectory ?? [];
|
|
577
555
|
const task = entry.task ?? "";
|
|
578
|
-
const maxRounds = lineage.max_rounds ?? getPolicy().
|
|
556
|
+
const maxRounds = lineage.max_rounds ?? getPolicy().engine.max_rounds;
|
|
579
557
|
const roundSnapshot = parseRoundTransactionSnapshot(lineage.round_snapshot);
|
|
580
558
|
const fallbackEvidence = EvidenceCollector.fromProviderNames(getPolicy().evidence.providers).collect();
|
|
581
559
|
const persistedEval = lineage.last_self_eval;
|
|
@@ -635,6 +613,7 @@ export class SessionManager {
|
|
|
635
613
|
round: currentRound,
|
|
636
614
|
prompt: null,
|
|
637
615
|
stopReason: status,
|
|
616
|
+
stopDetail: `Loop is not running (status: ${status}). It may have already completed or been stopped.`,
|
|
638
617
|
};
|
|
639
618
|
}
|
|
640
619
|
this.sessions.set(session.sessionId, session);
|
|
@@ -652,6 +631,7 @@ export class SessionManager {
|
|
|
652
631
|
session.roundSnapshot = prepared?.snapshot ?? prepareRoundTransaction(session.loopId, session.currentRound, []);
|
|
653
632
|
session.currentPrompt = prompt;
|
|
654
633
|
session.currentLevel = prepared?.level ?? "l2";
|
|
634
|
+
session.currentWarnings = prepared?.warnings ?? [];
|
|
655
635
|
policyMetrics.recordStrategy(session.loopId, session.currentLevel);
|
|
656
636
|
this.save(session);
|
|
657
637
|
return {
|
|
@@ -661,8 +641,7 @@ export class SessionManager {
|
|
|
661
641
|
prompt,
|
|
662
642
|
level: session.currentLevel,
|
|
663
643
|
roundSuccess: false,
|
|
664
|
-
|
|
665
|
-
warnings: parseWarnings(prompt),
|
|
644
|
+
warnings: (session.currentWarnings ?? []),
|
|
666
645
|
};
|
|
667
646
|
}
|
|
668
647
|
/** Auto-resume all "running" sessions from vault on server startup.
|
|
@@ -802,34 +781,30 @@ export class SessionManager {
|
|
|
802
781
|
}
|
|
803
782
|
});
|
|
804
783
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
784
|
+
// ── advanceUnlocked pipeline ───────────────────────────────────────────
|
|
785
|
+
//
|
|
786
|
+
// The method is split into focused private helpers so each phase of the
|
|
787
|
+
// round-boundary decision loop is independently readable and testable.
|
|
788
|
+
//
|
|
789
|
+
// Pipeline: validate → extract → execute transaction → route disposition
|
|
790
|
+
// → advance to next round (continue) or return terminal result.
|
|
791
|
+
/** Extract a SelfEvaluation from agent output.
|
|
792
|
+
* Structured param preferred → regex extraction → heuristic fallback.
|
|
793
|
+
* Returns null only when both regex and heuristic returned null. */
|
|
794
|
+
extractEvaluation(output, preExtractedEval) {
|
|
815
795
|
if (preExtractedEval) {
|
|
816
|
-
selfEval
|
|
817
|
-
extractionFailed = false;
|
|
818
|
-
}
|
|
819
|
-
else {
|
|
820
|
-
const structured = extractSelfEvaluation(output);
|
|
821
|
-
extractionFailed = structured === null;
|
|
822
|
-
selfEval = structured ?? heuristicSelfEvaluation(output);
|
|
823
|
-
}
|
|
824
|
-
// Guard: if both extraction methods returned null, stop
|
|
825
|
-
if (!selfEval) {
|
|
826
|
-
session.status = "stalled";
|
|
827
|
-
this.save(session);
|
|
828
|
-
void this.notifyTerminal(session, "stalled");
|
|
829
|
-
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason: "stalled", round: session.currentRound });
|
|
830
|
-
return { sessionId, round: session.currentRound, prompt: null, stopReason: "stalled", roundSuccess: false, quality: 0 };
|
|
796
|
+
return { selfEval: preExtractedEval, extractionFailed: false };
|
|
831
797
|
}
|
|
832
|
-
|
|
798
|
+
const structured = extractSelfEvaluation(output);
|
|
799
|
+
if (!structured)
|
|
800
|
+
return null;
|
|
801
|
+
return { selfEval: structured, extractionFailed: false };
|
|
802
|
+
}
|
|
803
|
+
/** Execute the round transaction and apply per-rule rejection tracking.
|
|
804
|
+
* MUTATES: session.roundSnapshot, session.consecutiveRejections,
|
|
805
|
+
* session.lastRejectionCheck, session.lastSelfEval,
|
|
806
|
+
* session.successTrajectory */
|
|
807
|
+
async executeRoundTransaction(session, selfEval, extractionFailed) {
|
|
833
808
|
const snapshot = session.roundSnapshot ?? prepareRoundTransaction(session.loopId, session.currentRound, session.evidenceBaseline ?? []);
|
|
834
809
|
const completed = await new RoundDriver(session.engine, this.backend).complete({
|
|
835
810
|
snapshot,
|
|
@@ -843,13 +818,12 @@ export class SessionManager {
|
|
|
843
818
|
successTrajectory: session.successTrajectory,
|
|
844
819
|
});
|
|
845
820
|
const outcome = completed.outcome;
|
|
846
|
-
const
|
|
821
|
+
const actualEvidence = completed.actualEvidence;
|
|
847
822
|
session.roundSnapshot = outcome.snapshot;
|
|
848
823
|
const pr = outcome.result;
|
|
849
824
|
policyMetrics.recordStrategyOutcome(session.loopId, session.currentLevel, pr, outcome.replayed);
|
|
850
|
-
|
|
851
|
-
//
|
|
852
|
-
// accumulate. A different rejection reason resets the counter.
|
|
825
|
+
// Per-rule rejection tracking: same-check rejections accumulate;
|
|
826
|
+
// a different rejection reason resets the counter.
|
|
853
827
|
if (pr.action === "reject" && pr.rejectionCheck) {
|
|
854
828
|
session.consecutiveRejections =
|
|
855
829
|
pr.rejectionCheck === session.lastRejectionCheck
|
|
@@ -867,75 +841,93 @@ export class SessionManager {
|
|
|
867
841
|
if (pr.shouldPushSuccessTrajectory) {
|
|
868
842
|
session.successTrajectory.push(pr.roundSuccess);
|
|
869
843
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
sessionId,
|
|
880
|
-
round: session.currentRound,
|
|
881
|
-
roundId: session.roundSnapshot.roundId,
|
|
882
|
-
prompt: null,
|
|
883
|
-
stopReason: "stalled",
|
|
884
|
-
};
|
|
885
|
-
}
|
|
886
|
-
session.roundSnapshot = preparedRetry.snapshot;
|
|
887
|
-
session.currentPrompt = preparedRetry.prompt;
|
|
888
|
-
session.currentLevel = preparedRetry.level;
|
|
889
|
-
this.save(session);
|
|
890
|
-
return {
|
|
891
|
-
sessionId,
|
|
892
|
-
round: session.currentRound,
|
|
893
|
-
roundId: session.roundSnapshot.roundId,
|
|
894
|
-
prompt: preparedRetry.prompt,
|
|
895
|
-
level: preparedRetry.level,
|
|
896
|
-
enforcementAction: "reject",
|
|
897
|
-
enforcementReason: pr.enforcementReason,
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
// Accepted rounds advance the before-snapshot. Rejected rounds retain the
|
|
901
|
-
// original baseline so their retry is still a zero-commit transaction.
|
|
902
|
-
session.evidenceBaseline = actualSnapshots;
|
|
903
|
-
if (pr.action === "terminate") {
|
|
904
|
-
session.status = "stopped";
|
|
844
|
+
return { pr, verificationFlags: pr.verificationFlags, actualEvidence };
|
|
845
|
+
}
|
|
846
|
+
/** Build a rejection result: compile a retry prompt, persist, return.
|
|
847
|
+
* MUTATES: session.roundSnapshot, session.currentPrompt, session.currentLevel */
|
|
848
|
+
async buildRejectionResult(sessionId, session, pr, verificationFlags) {
|
|
849
|
+
const retryRequest = buildLoopRequest(session, undefined, verificationFlags);
|
|
850
|
+
const preparedRetry = await new RoundDriver(session.engine, this.backend).prepareRetry(retryRequest, session.roundSnapshot, pr.rejectionPrompt ?? "", session.consecutiveRejections);
|
|
851
|
+
if (!preparedRetry) {
|
|
852
|
+
session.status = "stalled";
|
|
905
853
|
session.currentPrompt = null;
|
|
906
854
|
this.save(session);
|
|
907
|
-
void this.notifyTerminal(session, "enforcement_terminated");
|
|
908
|
-
logEvent("session_end", {
|
|
909
|
-
sessionId, loopId: session.loopId,
|
|
910
|
-
stopReason: "enforcement_terminated", round: session.currentRound,
|
|
911
|
-
});
|
|
912
855
|
return {
|
|
913
856
|
sessionId,
|
|
914
857
|
round: session.currentRound,
|
|
915
|
-
roundId: session.roundSnapshot
|
|
858
|
+
roundId: session.roundSnapshot?.roundId,
|
|
916
859
|
prompt: null,
|
|
917
|
-
stopReason: "
|
|
918
|
-
|
|
919
|
-
enforcementReason: pr.enforcementReason,
|
|
860
|
+
stopReason: "stalled",
|
|
861
|
+
stopDetail: "RoundDriver.prepareRetry returned null — the retry prompt could not be compiled.",
|
|
920
862
|
};
|
|
921
863
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
864
|
+
session.roundSnapshot = preparedRetry.snapshot;
|
|
865
|
+
session.currentPrompt = preparedRetry.prompt;
|
|
866
|
+
session.currentLevel = preparedRetry.level;
|
|
867
|
+
session.currentWarnings = preparedRetry.warnings ?? [];
|
|
868
|
+
this.save(session);
|
|
869
|
+
return {
|
|
870
|
+
sessionId,
|
|
871
|
+
round: session.currentRound,
|
|
872
|
+
roundId: session.roundSnapshot.roundId,
|
|
873
|
+
prompt: preparedRetry.prompt,
|
|
874
|
+
level: preparedRetry.level,
|
|
875
|
+
enforcementAction: "reject",
|
|
876
|
+
enforcementReason: pr.enforcementReason,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
/** Build a termination result: persist stopped status, notify sinks.
|
|
880
|
+
* MUTATES: session.status, session.currentPrompt */
|
|
881
|
+
buildTerminationResult(sessionId, session, pr) {
|
|
882
|
+
session.status = "stopped";
|
|
883
|
+
session.currentPrompt = null;
|
|
884
|
+
this.save(session);
|
|
885
|
+
void this.notifyTerminal(session, "enforcement_terminated");
|
|
886
|
+
logEvent("session_end", {
|
|
887
|
+
sessionId, loopId: session.loopId,
|
|
888
|
+
stopReason: "enforcement_terminated", round: session.currentRound,
|
|
889
|
+
});
|
|
890
|
+
return {
|
|
891
|
+
sessionId,
|
|
892
|
+
round: session.currentRound,
|
|
893
|
+
roundId: session.roundSnapshot?.roundId,
|
|
894
|
+
prompt: null,
|
|
895
|
+
stopReason: "enforcement_terminated",
|
|
896
|
+
stopDetail: pr.enforcementReason ?? "The enforcement gate terminated the loop.",
|
|
897
|
+
enforcementAction: "terminate",
|
|
898
|
+
enforcementReason: pr.enforcementReason,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
/** Build a stop result: persist stopped/stalled status, notify sinks.
|
|
902
|
+
* MUTATES: session.status, session.currentPrompt */
|
|
903
|
+
buildStopResult(sessionId, session, pr) {
|
|
904
|
+
const reason = pr.stopReason ?? "stalled";
|
|
905
|
+
session.status = reason === "stalled" ? "stalled" : "stopped";
|
|
906
|
+
session.currentPrompt = null;
|
|
907
|
+
this.save(session);
|
|
908
|
+
void this.notifyTerminal(session, reason);
|
|
909
|
+
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason: reason, round: session.currentRound });
|
|
910
|
+
return {
|
|
911
|
+
sessionId, round: session.currentRound,
|
|
912
|
+
roundId: session.roundSnapshot?.roundId,
|
|
913
|
+
prompt: null, stopReason: reason,
|
|
914
|
+
stopDetail: pr.stopReason === "circuit_breaker"
|
|
915
|
+
? `${pr.roundSuccess ? "No" : "All"} recent rounds failed, triggering the circuit breaker.`
|
|
916
|
+
: pr.stopReason === "max_rounds"
|
|
917
|
+
? "The configured maximum number of rounds has been reached."
|
|
918
|
+
: `Loop stopped: ${reason}.`,
|
|
919
|
+
roundSuccess: pr.roundSuccess,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
/** Compile the next round's prompt and advance the session.
|
|
923
|
+
* Includes the commit fence (pause/delete race guard) and context provider.
|
|
924
|
+
* MUTATES: session.currentRound, session.currentPrompt, session.currentLevel,
|
|
925
|
+
* session.evidenceBaseline, session.roundSnapshot */
|
|
926
|
+
async advanceToNextRound(sessionId, session, selfEval, pr, verificationFlags, actualEvidence) {
|
|
931
927
|
const roundSuccess = pr.roundSuccess;
|
|
932
|
-
// Feedback + decision metadata were committed by the transaction layer.
|
|
933
|
-
// Build deprecated quality alias for backward compat
|
|
934
|
-
const deprecatedQuality = roundSuccess ? 5 : 1;
|
|
935
|
-
// 4. Compile next round after the accepted commit.
|
|
936
928
|
session.currentRound++;
|
|
937
929
|
session.currentPrompt = null;
|
|
938
|
-
// v1.8: Memory injection
|
|
930
|
+
// v1.8: Memory injection — tier-aware external context.
|
|
939
931
|
let externalCtx = "";
|
|
940
932
|
if (this.contextProvider) {
|
|
941
933
|
try {
|
|
@@ -954,32 +946,31 @@ export class SessionManager {
|
|
|
954
946
|
});
|
|
955
947
|
}
|
|
956
948
|
}
|
|
957
|
-
// pause()/delete()
|
|
958
|
-
//
|
|
959
|
-
// session map + status as a commit fence so an in-flight advance cannot
|
|
960
|
-
// compile/save another round after a terminal lifecycle transition.
|
|
949
|
+
// Commit fence: pause()/delete() may have run while the context provider
|
|
950
|
+
// was awaited. Do not compile if the session is no longer running.
|
|
961
951
|
if (this.sessions.get(sessionId) !== session || session.status !== "running") {
|
|
962
952
|
return {
|
|
963
953
|
sessionId,
|
|
964
954
|
round: session.currentRound,
|
|
965
955
|
prompt: null,
|
|
966
956
|
stopReason: session.status,
|
|
957
|
+
stopDetail: `Session is no longer running (status: ${session.status}). It was paused or deleted while the context provider was running.`,
|
|
967
958
|
roundSuccess,
|
|
968
|
-
quality: deprecatedQuality,
|
|
969
959
|
};
|
|
970
960
|
}
|
|
971
|
-
const request = buildLoopRequest(session, selfEval,
|
|
961
|
+
const request = buildLoopRequest(session, selfEval, verificationFlags);
|
|
972
962
|
if (externalCtx) {
|
|
973
963
|
request.external_context = externalCtx;
|
|
974
964
|
}
|
|
975
965
|
const prepared = await new RoundDriver(session.engine, this.backend).prepare(request, session.loopId, session.currentRound);
|
|
976
966
|
const nextPrompt = prepared?.prompt ?? null;
|
|
977
967
|
const nextLevel = prepared?.level ?? "l2";
|
|
978
|
-
const nextBaseline = prepared?.evidenceBaseline ??
|
|
968
|
+
const nextBaseline = prepared?.evidenceBaseline ?? actualEvidence;
|
|
979
969
|
session.evidenceBaseline = nextBaseline;
|
|
980
970
|
session.roundSnapshot = prepared?.snapshot ?? prepareRoundTransaction(session.loopId, session.currentRound, nextBaseline);
|
|
981
971
|
session.currentPrompt = nextPrompt;
|
|
982
972
|
session.currentLevel = nextLevel;
|
|
973
|
+
session.currentWarnings = prepared?.warnings ?? [];
|
|
983
974
|
policyMetrics.recordStrategy(session.loopId, nextLevel);
|
|
984
975
|
this.save(session);
|
|
985
976
|
return {
|
|
@@ -989,10 +980,45 @@ export class SessionManager {
|
|
|
989
980
|
prompt: nextPrompt,
|
|
990
981
|
level: nextLevel,
|
|
991
982
|
roundSuccess,
|
|
992
|
-
|
|
993
|
-
warnings: parseWarnings(nextPrompt),
|
|
983
|
+
warnings: prepared?.warnings ?? [],
|
|
994
984
|
};
|
|
995
985
|
}
|
|
986
|
+
async advanceUnlocked(sessionId, output, preExtractedEval) {
|
|
987
|
+
// ── 1. Validate session ──────────────────────────────────────────────
|
|
988
|
+
const session = this.sessions.get(sessionId);
|
|
989
|
+
if (!session)
|
|
990
|
+
return { sessionId, round: 0, prompt: null, stopReason: "session_not_found", stopDetail: "No session exists with this ID. It may have expired, been deleted, or never existed." };
|
|
991
|
+
if (session.status !== "running") {
|
|
992
|
+
return { sessionId, round: session.currentRound, prompt: null, stopReason: session.status, stopDetail: `Session is ${session.status}. Use loopforge_resume to restart a paused session, or loopforge_start for a new task.` };
|
|
993
|
+
}
|
|
994
|
+
// ── 2. Extract self-evaluation ───────────────────────────────────────
|
|
995
|
+
const extracted = this.extractEvaluation(output, preExtractedEval);
|
|
996
|
+
if (!extracted) {
|
|
997
|
+
session.status = "stalled";
|
|
998
|
+
this.save(session);
|
|
999
|
+
void this.notifyTerminal(session, "stalled");
|
|
1000
|
+
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason: "stalled", round: session.currentRound });
|
|
1001
|
+
return { sessionId, round: session.currentRound, prompt: null, stopReason: "stalled", stopDetail: "Self-evaluation could not be extracted from agent output. Both structured regex and heuristic fallback returned null.", roundSuccess: false };
|
|
1002
|
+
}
|
|
1003
|
+
const { selfEval, extractionFailed } = extracted;
|
|
1004
|
+
// ── 3. Execute round transaction ─────────────────────────────────────
|
|
1005
|
+
const tx = await this.executeRoundTransaction(session, selfEval, extractionFailed);
|
|
1006
|
+
// ── 4. Route disposition ─────────────────────────────────────────────
|
|
1007
|
+
if (tx.pr.action === "reject") {
|
|
1008
|
+
return this.buildRejectionResult(sessionId, session, tx.pr, tx.verificationFlags);
|
|
1009
|
+
}
|
|
1010
|
+
// Accepted rounds advance the before-snapshot baseline. Rejected rounds
|
|
1011
|
+
// retain the original baseline so their retry remains zero-commit.
|
|
1012
|
+
session.evidenceBaseline = tx.actualEvidence;
|
|
1013
|
+
if (tx.pr.action === "terminate") {
|
|
1014
|
+
return this.buildTerminationResult(sessionId, session, tx.pr);
|
|
1015
|
+
}
|
|
1016
|
+
if (tx.pr.action === "stop") {
|
|
1017
|
+
return this.buildStopResult(sessionId, session, tx.pr);
|
|
1018
|
+
}
|
|
1019
|
+
// ── 5. Continue — compile next round ─────────────────────────────────
|
|
1020
|
+
return this.advanceToNextRound(sessionId, session, selfEval, tx.pr, tx.verificationFlags, tx.actualEvidence);
|
|
1021
|
+
}
|
|
996
1022
|
/** Write back loop knowledge to long-term memory.
|
|
997
1023
|
* Uses shared base builder from policy.ts. Called when a loop terminates. */
|
|
998
1024
|
async notifyTerminal(session, stopReason) {
|