loopforge 1.7.0 → 1.12.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 +64 -3
- package/dist/backends/fs.d.ts.map +1 -1
- package/dist/backends/fs.js +1 -2
- package/dist/backends/fs.js.map +1 -1
- package/dist/backends/interface.d.ts +2 -0
- package/dist/backends/interface.d.ts.map +1 -1
- package/dist/builder.d.ts +6 -10
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +128 -63
- package/dist/builder.js.map +1 -1
- package/dist/engine.d.ts +16 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +142 -54
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +33 -2
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +391 -135
- package/dist/loop-compiler.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +6 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +18 -4
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +225 -24
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +32 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +33 -14
- package/dist/mcp/tools.js.map +1 -1
- package/dist/memory-bridge.d.ts +47 -0
- package/dist/memory-bridge.d.ts.map +1 -0
- package/dist/memory-bridge.js +290 -0
- package/dist/memory-bridge.js.map +1 -0
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +1 -1
- package/dist/policy.d.ts +66 -9
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +41 -28
- package/dist/policy.js.map +1 -1
- package/dist/protocol.d.ts +120 -16
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +23 -9
- package/dist/protocol.js.map +1 -1
- package/dist/replay.js +7 -7
- package/dist/replay.js.map +1 -1
- package/dist/runtime.d.ts +20 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +420 -177
- package/dist/runtime.js.map +1 -1
- package/dist/tests/builder.test.js +40 -28
- package/dist/tests/builder.test.js.map +1 -1
- package/dist/tests/engine.test.js +13 -17
- package/dist/tests/engine.test.js.map +1 -1
- package/dist/tests/generate-schema.test.js +10 -7
- package/dist/tests/generate-schema.test.js.map +1 -1
- package/dist/tests/loop-compiler.test.js +481 -27
- package/dist/tests/loop-compiler.test.js.map +1 -1
- package/dist/tests/mcp.test.js +103 -103
- package/dist/tests/mcp.test.js.map +1 -1
- package/dist/tests/memory-bridge.test.d.ts +8 -0
- package/dist/tests/memory-bridge.test.d.ts.map +1 -0
- package/dist/tests/memory-bridge.test.js +383 -0
- package/dist/tests/memory-bridge.test.js.map +1 -0
- package/dist/tests/policy.test.js +5 -15
- package/dist/tests/policy.test.js.map +1 -1
- package/dist/tests/protocol.test.js +15 -3
- package/dist/tests/protocol.test.js.map +1 -1
- package/dist/tests/replay.test.js +9 -9
- package/dist/tests/replay.test.js.map +1 -1
- package/dist/tests/runtime.test.js +8 -8
- package/dist/tests/runtime.test.js.map +1 -1
- package/dist/tests/verification-gate.test.js +1 -1
- package/dist/tests/verification-gate.test.js.map +1 -1
- package/dist/verification-gate.d.ts +2 -2
- package/dist/verification-gate.js +2 -2
- package/loop_policy.json +28 -23
- package/package.json +1 -1
- package/skills/perception/SKILL.md +64 -12
- package/dist/adapter.d.ts +0 -17
- package/dist/adapter.d.ts.map +0 -1
- package/dist/adapter.js +0 -228
- package/dist/adapter.js.map +0 -1
- package/dist/tests/adapter.test.d.ts +0 -2
- package/dist/tests/adapter.test.d.ts.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Tests for loop compiler core: gates, advisories, L0/L1/L2 compilation. */
|
|
2
2
|
import { describe, it, beforeEach } from "node:test";
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
|
-
import { decideLevel, compileLoop, computeGoalTextHash, deriveGoalId, alignTask, compileL2, buildRollingSummary, formatRollingSummaryForPrompt, } from "../loop-compiler.js";
|
|
5
|
-
import { makeLoopCompileRequest, makeLoopRoundResult, makeLoopObjective, makeRollingSummary, } from "../protocol.js";
|
|
4
|
+
import { decideLevel, compileLoop, computeGoalTextHash, deriveGoalId, alignTask, compileL2, buildRollingSummary, formatRollingSummaryForPrompt, buildCheckpointSummary, formatCheckpointForPrompt, filterConstraintsForSubTask, formatDelegationPrompt, buildDelegationSummary, } from "../loop-compiler.js";
|
|
5
|
+
import { makeLoopCompileRequest, makeLoopRoundResult, makeLoopObjective, makeRollingSummary, makeCheckpointSummary, makeSelfEvaluation, } from "../protocol.js";
|
|
6
6
|
import { resetPolicy } from "../policy.js";
|
|
7
7
|
describe("Loop Compiler — Goal Identity", () => {
|
|
8
8
|
it("computeGoalTextHash produces a 12-char hex string", () => {
|
|
@@ -409,16 +409,17 @@ describe("Loop Compiler — Cross-Round Scenarios", () => {
|
|
|
409
409
|
// Failure Lineage Weighting (v1.7)
|
|
410
410
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
411
411
|
function makeVaultResult(loopId, round, qualityScore, task, outputSummary = "", techniqueUsed = "zero-shot", constraintViolations = []) {
|
|
412
|
+
const success = qualityScore >= 3;
|
|
412
413
|
return {
|
|
413
414
|
task,
|
|
414
415
|
output_summary: outputSummary,
|
|
415
416
|
constraint_violations: constraintViolations,
|
|
416
417
|
technique_used: techniqueUsed,
|
|
417
|
-
|
|
418
|
+
success,
|
|
418
419
|
loop_lineage: {
|
|
419
420
|
loop_id: loopId,
|
|
420
421
|
round,
|
|
421
|
-
|
|
422
|
+
success,
|
|
422
423
|
task,
|
|
423
424
|
technique_used: techniqueUsed,
|
|
424
425
|
},
|
|
@@ -437,12 +438,12 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
|
|
|
437
438
|
const rs = buildRollingSummary("loop-1", 4, vault);
|
|
438
439
|
assert.ok(rs !== null);
|
|
439
440
|
assert.equal(rs.failed_patterns?.length ?? 0, 0);
|
|
440
|
-
//
|
|
441
|
-
for (const
|
|
442
|
-
assert.ok(!
|
|
441
|
+
// key_outcomes should not have [Consider alternatives] marker
|
|
442
|
+
for (const ko of rs.key_outcomes) {
|
|
443
|
+
assert.ok(!ko.includes("[Consider alternatives]"));
|
|
443
444
|
}
|
|
444
445
|
});
|
|
445
|
-
it("detects 2 consecutive
|
|
446
|
+
it("detects 2 consecutive failed rounds with same technique and similar task", () => {
|
|
446
447
|
const vault = {
|
|
447
448
|
results: [
|
|
448
449
|
makeVaultResult("loop-1", 1, 2, "fix cache bug", "Tried cache fix A", "zero-shot"),
|
|
@@ -459,7 +460,7 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
|
|
|
459
460
|
assert.ok(fp.includes("cache"));
|
|
460
461
|
assert.ok(fp.includes("2 consecutive rounds"));
|
|
461
462
|
});
|
|
462
|
-
it("detects 3 consecutive
|
|
463
|
+
it("detects 3 consecutive failed rounds", () => {
|
|
463
464
|
const vault = {
|
|
464
465
|
results: [
|
|
465
466
|
makeVaultResult("loop-1", 1, 2, "fix memory leak", "Attempt 1", "few-shot"),
|
|
@@ -473,7 +474,7 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
|
|
|
473
474
|
assert.ok((rs.failed_patterns?.length ?? 0) >= 1);
|
|
474
475
|
assert.ok(rs.failed_patterns[0].includes("3 consecutive rounds"));
|
|
475
476
|
});
|
|
476
|
-
it("no pattern when
|
|
477
|
+
it("no pattern when failed rounds use different techniques", () => {
|
|
477
478
|
const vault = {
|
|
478
479
|
results: [
|
|
479
480
|
makeVaultResult("loop-1", 1, 2, "fix cache bug", "Attempt", "zero-shot"),
|
|
@@ -485,7 +486,7 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
|
|
|
485
486
|
assert.ok(rs !== null);
|
|
486
487
|
assert.equal(rs.failed_patterns?.length ?? 0, 0);
|
|
487
488
|
});
|
|
488
|
-
it("no pattern when
|
|
489
|
+
it("no pattern when failed tasks are completely different (Jaccard < 0.4)", () => {
|
|
489
490
|
const vault = {
|
|
490
491
|
results: [
|
|
491
492
|
makeVaultResult("loop-1", 1, 2, "fix cache bug in Redis layer", "Attempt", "zero-shot"),
|
|
@@ -497,7 +498,7 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
|
|
|
497
498
|
assert.ok(rs !== null);
|
|
498
499
|
assert.equal(rs.failed_patterns?.length ?? 0, 0);
|
|
499
500
|
});
|
|
500
|
-
it("
|
|
501
|
+
it("key_outcomes from failure-pattern rounds are demoted to end with [Consider alternatives]", () => {
|
|
501
502
|
const vault = {
|
|
502
503
|
results: [
|
|
503
504
|
makeVaultResult("loop-1", 1, 2, "fix cache bug", "Tried cache approach", "zero-shot"),
|
|
@@ -508,14 +509,15 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
|
|
|
508
509
|
};
|
|
509
510
|
const rs = buildRollingSummary("loop-1", 4, vault);
|
|
510
511
|
assert.ok(rs !== null);
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
assert.ok(kl.includes("[Consider alternatives]"));
|
|
512
|
+
// key_outcomes only include score>=4 rounds (failure-pattern rounds score<3 don't appear)
|
|
513
|
+
// Round 3 (score=5) should be present and not marked
|
|
514
|
+
for (const ko of rs.key_outcomes) {
|
|
515
|
+
if (ko.includes("cache")) {
|
|
516
|
+
assert.ok(ko.includes("[Consider alternatives]"));
|
|
517
517
|
}
|
|
518
518
|
}
|
|
519
|
+
// Round 3 (score=5) should be present with auth fix outcome
|
|
520
|
+
assert.ok(rs.key_outcomes.some((ko) => ko.includes("Auth")));
|
|
519
521
|
});
|
|
520
522
|
it("recurring_issues from failure-only rounds marked as [Possible dead end]", () => {
|
|
521
523
|
const vault = {
|
|
@@ -557,15 +559,12 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
|
|
|
557
559
|
describe("Failure Lineage Weighting — formatRollingSummaryForPrompt", () => {
|
|
558
560
|
it("renders failure patterns section when present", () => {
|
|
559
561
|
const rs = makeRollingSummary({
|
|
560
|
-
|
|
561
|
-
trajectory_direction: "improving",
|
|
562
|
-
what_worked: ["R3 (score=5): Fixed auth"],
|
|
562
|
+
key_outcomes: ["[R3] ✓ (step-back): Fixed auth bug"],
|
|
563
563
|
recurring_issues: [],
|
|
564
|
-
key_lessons: ["[R3] Fixed auth bug"],
|
|
565
564
|
rounds_sampled: 3,
|
|
566
565
|
generated_at_round: 4,
|
|
567
566
|
failed_patterns: [
|
|
568
|
-
"technique 'zero-shot' on task 'fix cache bug' failed 2 consecutive rounds (R1-R2
|
|
567
|
+
"technique 'zero-shot' on task 'fix cache bug' failed 2 consecutive rounds (R1-R2) — consider strategy change",
|
|
569
568
|
],
|
|
570
569
|
});
|
|
571
570
|
const text = formatRollingSummaryForPrompt(rs);
|
|
@@ -576,11 +575,8 @@ describe("Failure Lineage Weighting — formatRollingSummaryForPrompt", () => {
|
|
|
576
575
|
});
|
|
577
576
|
it("no failure patterns section when empty", () => {
|
|
578
577
|
const rs = makeRollingSummary({
|
|
579
|
-
|
|
580
|
-
trajectory_direction: "stable",
|
|
581
|
-
what_worked: [],
|
|
578
|
+
key_outcomes: [],
|
|
582
579
|
recurring_issues: [],
|
|
583
|
-
key_lessons: [],
|
|
584
580
|
rounds_sampled: 2,
|
|
585
581
|
generated_at_round: 3,
|
|
586
582
|
failed_patterns: [],
|
|
@@ -594,4 +590,462 @@ describe("Failure Lineage Weighting — formatRollingSummaryForPrompt", () => {
|
|
|
594
590
|
assert.equal(formatRollingSummaryForPrompt(empty), "");
|
|
595
591
|
});
|
|
596
592
|
});
|
|
593
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
594
|
+
// Delegation Helpers (v1.9 — AgentTool mode)
|
|
595
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
596
|
+
describe("Delegation Helpers — filterConstraintsForSubTask", () => {
|
|
597
|
+
const constraints = [
|
|
598
|
+
"all APIs must have rate limiting",
|
|
599
|
+
"use TypeScript strict mode",
|
|
600
|
+
"all files must have license header",
|
|
601
|
+
"every function needs unit tests",
|
|
602
|
+
"禁止使用 any 类型",
|
|
603
|
+
];
|
|
604
|
+
it("returns empty array for empty constraints", () => {
|
|
605
|
+
assert.deepEqual(filterConstraintsForSubTask([], "search for deprecated API"), []);
|
|
606
|
+
});
|
|
607
|
+
it("returns empty array for empty subTask", () => {
|
|
608
|
+
assert.deepEqual(filterConstraintsForSubTask(constraints, ""), []);
|
|
609
|
+
assert.deepEqual(filterConstraintsForSubTask(constraints, " "), []);
|
|
610
|
+
});
|
|
611
|
+
it("returns only relevant constraints by Jaccard similarity", () => {
|
|
612
|
+
// "search for deprecated API usage in src/" should NOT match
|
|
613
|
+
// rate limiting, strict mode, license headers, or unit tests
|
|
614
|
+
const result = filterConstraintsForSubTask(constraints, "search for deprecated API usage in src/");
|
|
615
|
+
assert.equal(result.length, 0);
|
|
616
|
+
});
|
|
617
|
+
it("returns relevant constraint when task overlaps", () => {
|
|
618
|
+
// "add rate limiting to all API endpoints" should match
|
|
619
|
+
// "all APIs must have rate limiting"
|
|
620
|
+
const result = filterConstraintsForSubTask(constraints, "add rate limiting to all API endpoints");
|
|
621
|
+
assert.ok(result.length >= 1);
|
|
622
|
+
assert.ok(result.some((c) => c.includes("rate limiting")));
|
|
623
|
+
});
|
|
624
|
+
it("returns at least one constraint when task has TypeScript+testing overlap", () => {
|
|
625
|
+
// "add TypeScript unit tests for all API functions" overlaps with
|
|
626
|
+
// "every function needs unit tests" (Jaccard ~0.2);
|
|
627
|
+
// "use TypeScript strict mode" overlap is too low (Jaccard ~0.1)
|
|
628
|
+
const result = filterConstraintsForSubTask(constraints, "add TypeScript unit tests for all API functions");
|
|
629
|
+
assert.ok(result.length >= 1);
|
|
630
|
+
assert.ok(result.some((c) => c.includes("unit tests")));
|
|
631
|
+
});
|
|
632
|
+
it("custom threshold filters more aggressively", () => {
|
|
633
|
+
// With threshold 0.5, should return very few constraints
|
|
634
|
+
const lenient = filterConstraintsForSubTask(constraints, "add rate limiting to all API endpoints", 0.15);
|
|
635
|
+
const strict = filterConstraintsForSubTask(constraints, "add rate limiting to all API endpoints", 0.5);
|
|
636
|
+
assert.ok(strict.length <= lenient.length);
|
|
637
|
+
});
|
|
638
|
+
it("supports CJK subTask with CJK constraints", () => {
|
|
639
|
+
const cjkConstraints = [
|
|
640
|
+
"所有API必须有速率限制",
|
|
641
|
+
"使用TypeScript严格模式",
|
|
642
|
+
];
|
|
643
|
+
// "搜索废弃的API" doesn't overlap with rate limiting or strict mode
|
|
644
|
+
const result = filterConstraintsForSubTask(cjkConstraints, "搜索废弃的API使用情况");
|
|
645
|
+
assert.equal(result.length, 0);
|
|
646
|
+
});
|
|
647
|
+
it("matches CJK constraint with overlapping CJK subTask", () => {
|
|
648
|
+
const cjkConstraints = [
|
|
649
|
+
"所有API必须有速率限制",
|
|
650
|
+
"所有文件必须有许可证头",
|
|
651
|
+
];
|
|
652
|
+
const result = filterConstraintsForSubTask(cjkConstraints, "为API添加速率限制");
|
|
653
|
+
assert.ok(result.length >= 1);
|
|
654
|
+
assert.ok(result.some((c) => c.includes("速率限制")));
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
describe("Delegation Helpers — formatDelegationPrompt", () => {
|
|
658
|
+
const subTask = "search for deprecated API usage in src/";
|
|
659
|
+
const constraints = ["all APIs must have rate limiting"];
|
|
660
|
+
it("generates self-contained explore prompt", () => {
|
|
661
|
+
const prompt = formatDelegationPrompt(subTask, "explore", []);
|
|
662
|
+
assert.ok(prompt.includes("### Delegated Task"));
|
|
663
|
+
assert.ok(prompt.includes(subTask));
|
|
664
|
+
assert.ok(prompt.includes("### Output Format"));
|
|
665
|
+
// Explore should NOT get constraints
|
|
666
|
+
assert.ok(!prompt.includes("### Relevant Constraints"));
|
|
667
|
+
// Must be self-contained
|
|
668
|
+
assert.ok(!prompt.match(/based on (above|previous|your findings)/i));
|
|
669
|
+
assert.ok(!prompt.includes("continue from"));
|
|
670
|
+
});
|
|
671
|
+
it("generates general-purpose prompt with constraints", () => {
|
|
672
|
+
const prompt = formatDelegationPrompt("fix the null pointer in src/auth/validate.ts:42", "general-purpose", constraints);
|
|
673
|
+
assert.ok(prompt.includes("### Delegated Task"));
|
|
674
|
+
assert.ok(prompt.includes("### Relevant Constraints"));
|
|
675
|
+
assert.ok(prompt.includes("rate limiting"));
|
|
676
|
+
assert.ok(prompt.includes("don't leave it half-done"));
|
|
677
|
+
// Must be self-contained
|
|
678
|
+
assert.ok(!prompt.match(/based on (above|previous|your findings)/i));
|
|
679
|
+
});
|
|
680
|
+
it("generates plan prompt with instructions", () => {
|
|
681
|
+
const prompt = formatDelegationPrompt("design authentication middleware", "plan", constraints, { context: "Current auth is JWT-based in src/auth/" });
|
|
682
|
+
assert.ok(prompt.includes("### Delegated Task"));
|
|
683
|
+
assert.ok(prompt.includes("### Context"));
|
|
684
|
+
assert.ok(prompt.includes("JWT-based"));
|
|
685
|
+
assert.ok(prompt.includes("### Relevant Constraints"));
|
|
686
|
+
assert.ok(prompt.includes("Critical files for implementation"));
|
|
687
|
+
assert.ok(prompt.includes("3-5 files"));
|
|
688
|
+
});
|
|
689
|
+
it("skips constraints section when empty", () => {
|
|
690
|
+
const prompt = formatDelegationPrompt(subTask, "general-purpose", []);
|
|
691
|
+
assert.ok(!prompt.includes("### Relevant Constraints"));
|
|
692
|
+
});
|
|
693
|
+
it("custom sub-agent type gets generic template", () => {
|
|
694
|
+
const prompt = formatDelegationPrompt(subTask, "custom-auditor", constraints);
|
|
695
|
+
assert.ok(prompt.includes("### Delegated Task"));
|
|
696
|
+
assert.ok(prompt.includes("### Relevant Constraints"));
|
|
697
|
+
assert.ok(prompt.includes("Complete the task above"));
|
|
698
|
+
});
|
|
699
|
+
it("output is self-contained — no parent-context references", () => {
|
|
700
|
+
const types = ["explore", "general-purpose", "plan", "custom"];
|
|
701
|
+
for (const t of types) {
|
|
702
|
+
const prompt = formatDelegationPrompt("do something", t, constraints);
|
|
703
|
+
// No references to parent conversation
|
|
704
|
+
assert.ok(!prompt.match(/based on (above|previous|your findings)/i), `${t}: should not reference parent context`);
|
|
705
|
+
assert.ok(!prompt.includes("continue from"), `${t}: should not say "continue from"`);
|
|
706
|
+
assert.ok(!prompt.includes("as discussed"), `${t}: should not say "as discussed"`);
|
|
707
|
+
assert.ok(!prompt.includes("see above"), `${t}: should not say "see above"`);
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
describe("Delegation — buildDelegationSummary", () => {
|
|
712
|
+
it("returns empty string for null vault context", () => {
|
|
713
|
+
assert.equal(buildDelegationSummary(null), "");
|
|
714
|
+
});
|
|
715
|
+
it("returns empty string when no delegation journal entries", () => {
|
|
716
|
+
const vault = { results: [{ task_type: "loop_lineage", loop_id: "test" }] };
|
|
717
|
+
assert.equal(buildDelegationSummary(vault), "");
|
|
718
|
+
});
|
|
719
|
+
it("returns empty string when results array is empty", () => {
|
|
720
|
+
const vault = { results: [] };
|
|
721
|
+
assert.equal(buildDelegationSummary(vault), "");
|
|
722
|
+
});
|
|
723
|
+
it("builds delegation table from journal entries", () => {
|
|
724
|
+
const vault = {
|
|
725
|
+
results: [
|
|
726
|
+
{
|
|
727
|
+
task_type: "delegation_journal",
|
|
728
|
+
loop_lineage: {
|
|
729
|
+
round: 2,
|
|
730
|
+
delegations: [
|
|
731
|
+
{ index: 1, agentId: "abc-123", subAgentType: "explore", subTask: "search deprecated APIs", resultSummary: "found 12 instances", success: true, discoveredConstraints: [] },
|
|
732
|
+
{ index: 2, agentId: "def-456", subAgentType: "general-purpose", subTask: "fix auth bug", resultSummary: "fixed null pointer", success: true, discoveredConstraints: [] },
|
|
733
|
+
],
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
],
|
|
737
|
+
};
|
|
738
|
+
const text = buildDelegationSummary(vault);
|
|
739
|
+
assert.ok(text.includes("### Delegation History"));
|
|
740
|
+
assert.ok(text.includes("R2"));
|
|
741
|
+
assert.ok(text.includes("search deprecated APIs"));
|
|
742
|
+
assert.ok(text.includes("fix auth bug"));
|
|
743
|
+
assert.ok(text.includes("✓"));
|
|
744
|
+
// Agent column should display agentId
|
|
745
|
+
assert.ok(text.includes("abc-123"));
|
|
746
|
+
assert.ok(text.includes("def-456"));
|
|
747
|
+
// Table header should include Agent column
|
|
748
|
+
assert.ok(text.includes("| Round | Agent | Type | Task | Result | ✓/✗ |"));
|
|
749
|
+
});
|
|
750
|
+
it("shows ✗ for failed delegations", () => {
|
|
751
|
+
const vault = {
|
|
752
|
+
results: [
|
|
753
|
+
{
|
|
754
|
+
task_type: "delegation_journal",
|
|
755
|
+
loop_lineage: {
|
|
756
|
+
round: 3,
|
|
757
|
+
delegations: [
|
|
758
|
+
{ index: 1, agentId: "worker-xyz", subAgentType: "general-purpose", subTask: "fix complex bug", resultSummary: "failed — wrong approach", success: false, discoveredConstraints: [] },
|
|
759
|
+
],
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
],
|
|
763
|
+
};
|
|
764
|
+
const text = buildDelegationSummary(vault);
|
|
765
|
+
assert.ok(text.includes("✗"));
|
|
766
|
+
assert.ok(text.includes("fix complex bug"));
|
|
767
|
+
assert.ok(text.includes("wrong approach"));
|
|
768
|
+
});
|
|
769
|
+
it("handles multiple rounds of delegation", () => {
|
|
770
|
+
const vault = {
|
|
771
|
+
results: [
|
|
772
|
+
{
|
|
773
|
+
task_type: "delegation_journal",
|
|
774
|
+
loop_lineage: { round: 1, delegations: [{ index: 1, agentId: "agent-a", subAgentType: "explore", subTask: "research X", resultSummary: "done", success: true, discoveredConstraints: [] }] },
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
task_type: "delegation_journal",
|
|
778
|
+
loop_lineage: { round: 2, delegations: [{ index: 1, agentId: "agent-b", subAgentType: "general-purpose", subTask: "fix Y", resultSummary: "done", success: true, discoveredConstraints: [] }] },
|
|
779
|
+
},
|
|
780
|
+
],
|
|
781
|
+
};
|
|
782
|
+
const text = buildDelegationSummary(vault);
|
|
783
|
+
assert.ok(text.includes("R1"));
|
|
784
|
+
assert.ok(text.includes("R2"));
|
|
785
|
+
});
|
|
786
|
+
it("falls back to worker-N when agentId is missing (backward compat)", () => {
|
|
787
|
+
const vault = {
|
|
788
|
+
results: [
|
|
789
|
+
{
|
|
790
|
+
task_type: "delegation_journal",
|
|
791
|
+
loop_lineage: {
|
|
792
|
+
round: 1,
|
|
793
|
+
delegations: [
|
|
794
|
+
{ index: 3, subAgentType: "explore", subTask: "scan codebase", resultSummary: "done", success: true },
|
|
795
|
+
],
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
],
|
|
799
|
+
};
|
|
800
|
+
const text = buildDelegationSummary(vault);
|
|
801
|
+
assert.ok(text.includes("worker-3"), "should fall back to worker-3");
|
|
802
|
+
});
|
|
803
|
+
it("escapes pipe characters in markdown table cells", () => {
|
|
804
|
+
const vault = {
|
|
805
|
+
results: [
|
|
806
|
+
{
|
|
807
|
+
task_type: "delegation_journal",
|
|
808
|
+
loop_lineage: {
|
|
809
|
+
round: 1,
|
|
810
|
+
delegations: [
|
|
811
|
+
{ index: 1, agentId: "agent-1", subAgentType: "explore", subTask: "audit A|B testing in src/", resultSummary: "found issues in x|y module", success: true, discoveredConstraints: [] },
|
|
812
|
+
],
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
],
|
|
816
|
+
};
|
|
817
|
+
const text = buildDelegationSummary(vault);
|
|
818
|
+
// Should contain escaped pipes, not raw pipes that would break the table
|
|
819
|
+
assert.ok(text.includes("A\\|B"), "pipe in task should be escaped");
|
|
820
|
+
assert.ok(text.includes("x\\|y"), "pipe in result should be escaped");
|
|
821
|
+
// Verify the table row still has the correct number of pipe chars:
|
|
822
|
+
// 7 structural (leading + 5 separators + trailing) + 2 escaped inside cells = 9 total
|
|
823
|
+
const tableRow = text.split("\n").find((l) => l.startsWith("| R1"));
|
|
824
|
+
assert.ok(tableRow, "should have a table row for R1");
|
|
825
|
+
const pipeCount = (tableRow.match(/\|/g) ?? []).length;
|
|
826
|
+
assert.equal(pipeCount, 9, "table row should have 9 pipe chars (7 structural + 2 escaped)");
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
830
|
+
// Checkpoint Compression (v1.10)
|
|
831
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
832
|
+
describe("Checkpoint — buildCheckpointSummary", () => {
|
|
833
|
+
it("builds from self-eval with checkpoint fields", () => {
|
|
834
|
+
const selfEval = makeSelfEvaluation({
|
|
835
|
+
success: true,
|
|
836
|
+
output_summary: "Completed ERC20 data model with full test coverage",
|
|
837
|
+
compression_checkpoint: true,
|
|
838
|
+
checkpoint_label: "数据模型层完成",
|
|
839
|
+
});
|
|
840
|
+
const cs = buildCheckpointSummary(selfEval, 8, ["constraint-A", "constraint-B", "constraint-C"], ["old-constraint"]);
|
|
841
|
+
assert.ok(cs !== null);
|
|
842
|
+
assert.equal(cs.label, "数据模型层完成");
|
|
843
|
+
assert.equal(cs.declared_at_round, 8);
|
|
844
|
+
assert.ok(cs.outcome.includes("ERC20 data model"));
|
|
845
|
+
assert.deepStrictEqual(cs.carried_constraints, ["constraint-A", "constraint-B", "constraint-C"]);
|
|
846
|
+
assert.deepStrictEqual(cs.resolved_constraints, ["old-constraint"]);
|
|
847
|
+
});
|
|
848
|
+
it("falls back to auto-generated label when checkpoint_label is empty", () => {
|
|
849
|
+
const selfEval = makeSelfEvaluation({
|
|
850
|
+
success: true,
|
|
851
|
+
output_summary: "Done with auth module",
|
|
852
|
+
compression_checkpoint: true,
|
|
853
|
+
checkpoint_label: "",
|
|
854
|
+
});
|
|
855
|
+
const cs = buildCheckpointSummary(selfEval, 5, ["c1"], []);
|
|
856
|
+
assert.ok(cs.label.includes("Subtask completed at round 5"));
|
|
857
|
+
});
|
|
858
|
+
it("caps carried and resolved constraints at policy limit", () => {
|
|
859
|
+
const selfEval = makeSelfEvaluation({
|
|
860
|
+
success: true,
|
|
861
|
+
output_summary: "Done",
|
|
862
|
+
compression_checkpoint: true,
|
|
863
|
+
checkpoint_label: "test",
|
|
864
|
+
});
|
|
865
|
+
const manyConstraints = Array.from({ length: 15 }, (_, i) => `c${i + 1}`);
|
|
866
|
+
const cs = buildCheckpointSummary(selfEval, 3, manyConstraints, manyConstraints);
|
|
867
|
+
// Default policy: max_carried_constraints = 10
|
|
868
|
+
assert.equal(cs.carried_constraints.length, 10);
|
|
869
|
+
assert.equal(cs.resolved_constraints.length, 10);
|
|
870
|
+
});
|
|
871
|
+
it("truncates outcome at policy limit", () => {
|
|
872
|
+
const selfEval = makeSelfEvaluation({
|
|
873
|
+
success: true,
|
|
874
|
+
output_summary: "x".repeat(300),
|
|
875
|
+
compression_checkpoint: true,
|
|
876
|
+
checkpoint_label: "test",
|
|
877
|
+
});
|
|
878
|
+
const cs = buildCheckpointSummary(selfEval, 1, [], []);
|
|
879
|
+
// Default policy: outcome_max_chars = 200
|
|
880
|
+
assert.ok(cs.outcome.length <= 200);
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
describe("Checkpoint — formatCheckpointForPrompt", () => {
|
|
884
|
+
it("renders checkpoint block with all fields", () => {
|
|
885
|
+
const cs = makeCheckpointSummary({
|
|
886
|
+
label: "Auth module complete",
|
|
887
|
+
declared_at_round: 5,
|
|
888
|
+
outcome: "Implemented JWT auth with refresh tokens",
|
|
889
|
+
carried_constraints: ["所有API需要认证", "token过期时间≤24h"],
|
|
890
|
+
resolved_constraints: ["旧密码哈希方式需更新"],
|
|
891
|
+
});
|
|
892
|
+
const text = formatCheckpointForPrompt(cs);
|
|
893
|
+
assert.ok(text.includes("### Checkpoint: Auth module complete (Round 5)"));
|
|
894
|
+
assert.ok(text.includes("Implemented JWT auth"));
|
|
895
|
+
assert.ok(text.includes("所有API需要认证"));
|
|
896
|
+
assert.ok(text.includes("旧密码哈希方式需更新"));
|
|
897
|
+
assert.ok(text.includes("Carried Constraints"));
|
|
898
|
+
assert.ok(text.includes("Resolved Constraints"));
|
|
899
|
+
});
|
|
900
|
+
it("returns empty string for null", () => {
|
|
901
|
+
assert.equal(formatCheckpointForPrompt(null), "");
|
|
902
|
+
});
|
|
903
|
+
it("returns empty string for empty checkpoint", () => {
|
|
904
|
+
const cs = makeCheckpointSummary({});
|
|
905
|
+
assert.equal(formatCheckpointForPrompt(cs), "");
|
|
906
|
+
});
|
|
907
|
+
it("omits resolved section when no resolved constraints", () => {
|
|
908
|
+
const cs = makeCheckpointSummary({
|
|
909
|
+
label: "test",
|
|
910
|
+
declared_at_round: 1,
|
|
911
|
+
outcome: "done",
|
|
912
|
+
carried_constraints: ["c1"],
|
|
913
|
+
resolved_constraints: [],
|
|
914
|
+
});
|
|
915
|
+
const text = formatCheckpointForPrompt(cs);
|
|
916
|
+
assert.ok(!text.includes("Resolved Constraints"));
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
describe("Checkpoint — buildRollingSummary with sinceRound", () => {
|
|
920
|
+
function makeVaultResult(loopId, round, qualityScore, task, outputSummary = "", techniqueUsed = "zero-shot", constraintViolations = []) {
|
|
921
|
+
const success = qualityScore >= 3;
|
|
922
|
+
return {
|
|
923
|
+
task, output_summary: outputSummary,
|
|
924
|
+
constraint_violations: constraintViolations,
|
|
925
|
+
technique_used: techniqueUsed, success,
|
|
926
|
+
loop_lineage: { loop_id: loopId, round, success, task, technique_used: techniqueUsed },
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
it("only includes rounds at or after sinceRound", () => {
|
|
930
|
+
const vault = {
|
|
931
|
+
results: [
|
|
932
|
+
makeVaultResult("loop-1", 1, 4, "task A", "did A"),
|
|
933
|
+
makeVaultResult("loop-1", 2, 5, "task B", "did B"),
|
|
934
|
+
makeVaultResult("loop-1", 3, 3, "task C", "did C"),
|
|
935
|
+
makeVaultResult("loop-1", 4, 5, "task D", "did D"),
|
|
936
|
+
makeVaultResult("loop-1", 5, 4, "task E", "did E"),
|
|
937
|
+
],
|
|
938
|
+
};
|
|
939
|
+
const rs = buildRollingSummary("loop-1", 6, vault, 3);
|
|
940
|
+
assert.ok(rs !== null);
|
|
941
|
+
// Round 1 and 2 should be excluded (before sinceRound=3)
|
|
942
|
+
assert.equal(rs.rounds_sampled, 3);
|
|
943
|
+
// Verify key_outcomes contains rounds 3-5
|
|
944
|
+
assert.equal(rs.key_outcomes.length, 3);
|
|
945
|
+
assert.ok(rs.key_outcomes.some((k) => k.includes("R3")));
|
|
946
|
+
assert.ok(rs.key_outcomes.some((k) => k.includes("R5")));
|
|
947
|
+
});
|
|
948
|
+
it("sinceRound with no matching rounds returns null", () => {
|
|
949
|
+
const vault = {
|
|
950
|
+
results: [
|
|
951
|
+
makeVaultResult("loop-1", 1, 4, "task A", "did A"),
|
|
952
|
+
makeVaultResult("loop-1", 2, 5, "task B", "did B"),
|
|
953
|
+
],
|
|
954
|
+
};
|
|
955
|
+
const rs = buildRollingSummary("loop-1", 3, vault, 5);
|
|
956
|
+
assert.equal(rs, null);
|
|
957
|
+
});
|
|
958
|
+
it("without sinceRound includes all rounds (backward compat)", () => {
|
|
959
|
+
const vault = {
|
|
960
|
+
results: [
|
|
961
|
+
makeVaultResult("loop-1", 1, 4, "task A", "did A"),
|
|
962
|
+
makeVaultResult("loop-1", 2, 5, "task B", "did B"),
|
|
963
|
+
],
|
|
964
|
+
};
|
|
965
|
+
const rs = buildRollingSummary("loop-1", 3, vault);
|
|
966
|
+
assert.ok(rs !== null);
|
|
967
|
+
assert.equal(rs.rounds_sampled, 2);
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
describe("Checkpoint — compileL2 integration", () => {
|
|
971
|
+
it("renders checkpoint in prompt when last_round_result has checkpoint", () => {
|
|
972
|
+
const lr = makeLoopRoundResult({
|
|
973
|
+
round: 4,
|
|
974
|
+
success: true,
|
|
975
|
+
output_summary: "Completed data model layer",
|
|
976
|
+
constraint_violations: [],
|
|
977
|
+
compression_checkpoint: true,
|
|
978
|
+
checkpoint_label: "Data model done",
|
|
979
|
+
});
|
|
980
|
+
const req = makeLoopCompileRequest({
|
|
981
|
+
round: 5,
|
|
982
|
+
loop_id: "test-cp",
|
|
983
|
+
task: "Build API layer on top of data model",
|
|
984
|
+
last_round_result: lr,
|
|
985
|
+
});
|
|
986
|
+
// Empty vault — no prior checkpoint to load
|
|
987
|
+
const vault = { results: [], global_entries: [] };
|
|
988
|
+
const response = compileL2(req, vault);
|
|
989
|
+
assert.ok(response.prompt.includes("### Checkpoint: Data model done (Round 4)"));
|
|
990
|
+
assert.ok(response.prompt.includes("Completed data model layer"));
|
|
991
|
+
assert.ok(response.checkpoint_summary !== null);
|
|
992
|
+
assert.equal(response.checkpoint_summary.declared_at_round, 4);
|
|
993
|
+
});
|
|
994
|
+
it("no checkpoint in prompt when last_round_result has no checkpoint", () => {
|
|
995
|
+
const lr = makeLoopRoundResult({
|
|
996
|
+
round: 2,
|
|
997
|
+
success: true,
|
|
998
|
+
output_summary: "Fixed a bug",
|
|
999
|
+
constraint_violations: [],
|
|
1000
|
+
});
|
|
1001
|
+
const req = makeLoopCompileRequest({
|
|
1002
|
+
round: 3,
|
|
1003
|
+
loop_id: "test-nocp",
|
|
1004
|
+
task: "Continue fixing bugs",
|
|
1005
|
+
last_round_result: lr,
|
|
1006
|
+
});
|
|
1007
|
+
const vault = { results: [], global_entries: [] };
|
|
1008
|
+
const response = compileL2(req, vault);
|
|
1009
|
+
assert.ok(!response.prompt.includes("### Checkpoint:"));
|
|
1010
|
+
assert.equal(response.checkpoint_summary, null);
|
|
1011
|
+
});
|
|
1012
|
+
it("loads existing checkpoint from vault on later rounds", () => {
|
|
1013
|
+
// Simulate a previous round that stored a checkpoint in vault
|
|
1014
|
+
const vault = {
|
|
1015
|
+
results: [
|
|
1016
|
+
{
|
|
1017
|
+
loop_lineage: { loop_id: "test-cp2", round: 3, success: true },
|
|
1018
|
+
checkpoint_summary: {
|
|
1019
|
+
label: "API layer done",
|
|
1020
|
+
declared_at_round: 3,
|
|
1021
|
+
outcome: "Built REST API",
|
|
1022
|
+
carried_constraints: ["auth required"],
|
|
1023
|
+
resolved_constraints: [],
|
|
1024
|
+
},
|
|
1025
|
+
task: "Build API",
|
|
1026
|
+
output_summary: "Built REST API",
|
|
1027
|
+
technique_used: "least-to-most",
|
|
1028
|
+
},
|
|
1029
|
+
],
|
|
1030
|
+
};
|
|
1031
|
+
const lr = makeLoopRoundResult({
|
|
1032
|
+
round: 4,
|
|
1033
|
+
success: true,
|
|
1034
|
+
output_summary: "Added tests",
|
|
1035
|
+
constraint_violations: [],
|
|
1036
|
+
});
|
|
1037
|
+
const req = makeLoopCompileRequest({
|
|
1038
|
+
round: 5,
|
|
1039
|
+
loop_id: "test-cp2",
|
|
1040
|
+
task: "Add integration tests",
|
|
1041
|
+
last_round_result: lr,
|
|
1042
|
+
});
|
|
1043
|
+
const response = compileL2(req, vault);
|
|
1044
|
+
// Should load checkpoint from round 3
|
|
1045
|
+
assert.ok(response.prompt.includes("### Checkpoint: API layer done (Round 3)"));
|
|
1046
|
+
assert.ok(response.prompt.includes("Built REST API"));
|
|
1047
|
+
assert.ok(response.checkpoint_summary !== null);
|
|
1048
|
+
assert.equal(response.checkpoint_summary.declared_at_round, 3);
|
|
1049
|
+
});
|
|
1050
|
+
});
|
|
597
1051
|
//# sourceMappingURL=loop-compiler.test.js.map
|