openhermes 4.12.1 → 4.13.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/CONTEXT.md +6 -6
- package/ETHOS.md +2 -2
- package/README.md +11 -17
- package/bootstrap.ts +118 -126
- package/docs/HOW-IT-WORKS.md +162 -0
- package/docs/adr/ADR-0001-rebuild-vs-increment.md +30 -0
- package/docs/adr/ADR-0002-routing-graph-vs-linear-chain.md +36 -0
- package/docs/adr/ADR-0003-per-directory-plan-storage.md +34 -0
- package/docs/adr/ADR-0004-composer-fragment-architecture.md +42 -0
- package/docs/adr/ADR-0005-hook-system-design.md +42 -0
- package/docs/adr/README.md +9 -0
- package/harness/codex/AUTOPILOT.md +35 -40
- package/harness/codex/CHARTER.md +3 -3
- package/harness/lib/composer/compose.test.ts +29 -29
- package/harness/lib/composer/fragments/02-delegation.md +5 -5
- package/harness/lib/composer/fragments/04-task-flow.md +13 -13
- package/harness/lib/composer/fragments/08-routing.md +1 -1
- package/harness/lib/composer/fragments/09-guardrails.md +25 -25
- package/harness/lib/composer/index.ts +1 -1
- package/harness/lib/guards/guard-config.ts +72 -72
- package/harness/lib/hooks/builtins/confidence-gate-hook.ts +9 -9
- package/harness/lib/hooks/builtins/delegation-depth-hook.ts +1 -1
- package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -99
- package/harness/lib/hooks/builtins/next-route-hook.ts +24 -24
- package/harness/lib/hooks/builtins/plan-check-hook.ts +5 -5
- package/harness/lib/hooks/builtins/route-tracking-hook.ts +1 -1
- package/harness/lib/hooks/hooks.test.ts +160 -324
- package/harness/lib/hooks/index.ts +38 -42
- package/harness/lib/hooks/registry.ts +309 -416
- package/harness/lib/hooks/types.ts +116 -119
- package/harness/lib/plans/plan-location.ts +134 -134
- package/harness/lib/routing/index.ts +21 -21
- package/harness/lib/routing/route-guidance.ts +147 -147
- package/harness/lib/routing/route-resolver.ts +58 -58
- package/harness/lib/routing/routing.test.ts +195 -195
- package/harness/lib/routing/skill-frontmatter.ts +125 -125
- package/harness/lib/routing/types.ts +52 -52
- package/harness/skills/oh-ascii/SKILL.md +1 -1
- package/harness/skills/oh-fusion/DEEP.md +109 -109
- package/harness/skills/oh-fusion/SKILL.md +47 -47
- package/harness/skills/oh-init/DEEP.md +2 -2
- package/harness/skills/oh-plan-review/DEEP.md +1 -1
- package/harness/skills/oh-planner/DEEP.md +3 -3
- package/harness/skills/oh-review/DEEP.md +5 -5
- package/package.json +56 -53
- package/harness/lib/background/background.test.ts +0 -216
- package/harness/lib/background/index.ts +0 -7
- package/harness/lib/background/interfaces.ts +0 -31
- package/harness/lib/background/manager.ts +0 -320
- package/harness/lib/hooks/builtins/error-recovery-hook.ts +0 -107
- package/harness/lib/hooks/builtins/memory-sync-hook.ts +0 -73
- package/harness/lib/hooks/builtins/sanity-check-hook.ts +0 -52
- package/harness/lib/hooks/builtins/subagent-failure-hook.ts +0 -93
- package/harness/lib/memory/index.ts +0 -18
- package/harness/lib/memory/interfaces.ts +0 -53
- package/harness/lib/memory/memory-manager.ts +0 -205
- package/harness/lib/memory/memory.test.ts +0 -485
- package/harness/lib/memory/plan-store.ts +0 -346
- package/harness/lib/recovery/handler.ts +0 -243
- package/harness/lib/recovery/index.ts +0 -14
- package/harness/lib/recovery/interfaces.ts +0 -48
- package/harness/lib/recovery/patterns.ts +0 -149
- package/harness/lib/recovery/recovery.test.ts +0 -312
- package/harness/lib/sanity/anomaly-tracker.ts +0 -127
- package/harness/lib/sanity/checker.ts +0 -189
- package/harness/lib/sanity/index.ts +0 -13
- package/harness/lib/sanity/interfaces.ts +0 -24
- package/harness/lib/sanity/sanity.test.ts +0 -472
- package/harness/lib/sync/file-watcher.ts +0 -175
- package/harness/lib/sync/index.ts +0 -11
- package/harness/lib/sync/interfaces.ts +0 -27
- package/harness/lib/sync/plan-sync.ts +0 -533
- package/harness/lib/sync/sync.test.ts +0 -858
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// MemorySyncHook — PostToolUse, priority=40, phase=LATE
|
|
3
|
-
//
|
|
4
|
-
// After each step, sync memory entries to plan file.
|
|
5
|
-
// Uses MemoryManager from harness/lib/memory/memory-manager.ts
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
import { HookPhase, HookResult } from "../types.ts";
|
|
9
|
-
import type { HookContext, PostToolUseHook } from "../types.ts";
|
|
10
|
-
import { MemoryManager } from "../../memory/memory-manager.ts";
|
|
11
|
-
import { PlanStore } from "../../memory/plan-store.ts";
|
|
12
|
-
import { resolvePlanAccess } from "../../plans/plan-location.ts";
|
|
13
|
-
import { MemoryLevel } from "../../memory/interfaces.ts";
|
|
14
|
-
|
|
15
|
-
export const memorySyncHook: PostToolUseHook = {
|
|
16
|
-
metadata: {
|
|
17
|
-
name: "memory-sync",
|
|
18
|
-
priority: 40,
|
|
19
|
-
phase: HookPhase.LATE,
|
|
20
|
-
dependencies: [],
|
|
21
|
-
errorHandling: "isolate",
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
async execute(context: HookContext, output: string) {
|
|
25
|
-
// Sync memory entries to plan file
|
|
26
|
-
const planFile = resolvePlanAccess(context.directory)?.path ?? null;
|
|
27
|
-
if (!planFile) {
|
|
28
|
-
// No plan file to sync to — skip silently
|
|
29
|
-
return { result: HookResult.CONTINUE };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const mem = MemoryManager.getInstance();
|
|
33
|
-
|
|
34
|
-
// Extract findings from the session memory (TASK level)
|
|
35
|
-
const taskEntries = mem.getEntries(MemoryLevel.TASK);
|
|
36
|
-
|
|
37
|
-
// Sync important task entries as plan findings
|
|
38
|
-
for (const entry of taskEntries) {
|
|
39
|
-
if (entry.importance >= 0.6) {
|
|
40
|
-
try {
|
|
41
|
-
await PlanStore.addFinding(planFile, context.sessionId, {
|
|
42
|
-
description: entry.content,
|
|
43
|
-
severity: entry.importance >= 0.8 ? "warning" : "info",
|
|
44
|
-
});
|
|
45
|
-
} catch {
|
|
46
|
-
// Plan sync is best-effort — don't break execution
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Sync any decisions
|
|
52
|
-
const missionEntries = mem.getEntries(MemoryLevel.MISSION);
|
|
53
|
-
for (const entry of missionEntries) {
|
|
54
|
-
if (entry.metadata?.type === "decision" && entry.importance >= 0.7) {
|
|
55
|
-
try {
|
|
56
|
-
await PlanStore.addDecision(planFile, context.sessionId, {
|
|
57
|
-
description: entry.content,
|
|
58
|
-
rationale: (entry.metadata.rationale as string) ?? "Auto-synced decision",
|
|
59
|
-
});
|
|
60
|
-
} catch {
|
|
61
|
-
// Best-effort
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
result: HookResult.CONTINUE,
|
|
68
|
-
modifiedContext: {
|
|
69
|
-
_memorySyncCount: taskEntries.filter((e) => e.importance >= 0.6).length,
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
},
|
|
73
|
-
};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// SanityCheckHook — PostToolUse, priority=30, phase=LATE
|
|
3
|
-
//
|
|
4
|
-
// After each tool invocation, check the output for LLM degeneration patterns
|
|
5
|
-
// (repetition, low diversity, gibberish, etc.). Track consecutive anomalies
|
|
6
|
-
// per session. If 2+ consecutive anomalies detected, inject a recovery
|
|
7
|
-
// instruction to compact/refresh context before it cascades.
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
import { HookPhase, HookResult } from "../types.ts";
|
|
11
|
-
import type { HookContext, PostToolUseHook } from "../types.ts";
|
|
12
|
-
import { checkOutputSanity } from "../../sanity/checker.ts";
|
|
13
|
-
import { AnomalyTracker } from "../../sanity/anomaly-tracker.ts";
|
|
14
|
-
|
|
15
|
-
export const sanityCheckHook: PostToolUseHook = {
|
|
16
|
-
metadata: {
|
|
17
|
-
name: "sanity-check",
|
|
18
|
-
priority: 30,
|
|
19
|
-
phase: HookPhase.LATE,
|
|
20
|
-
dependencies: [],
|
|
21
|
-
errorHandling: "isolate",
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
async execute(context: HookContext, output: string) {
|
|
25
|
-
const sessionId = context.sessionId;
|
|
26
|
-
|
|
27
|
-
// Run the sanity checker on the output
|
|
28
|
-
const result = checkOutputSanity(output);
|
|
29
|
-
|
|
30
|
-
// Record the result in the anomaly tracker
|
|
31
|
-
const tracker = AnomalyTracker.getInstance();
|
|
32
|
-
const tracking = tracker.record(sessionId, result);
|
|
33
|
-
|
|
34
|
-
if (result.isHealthy) {
|
|
35
|
-
// Output is healthy — no action needed
|
|
36
|
-
return { result: HookResult.CONTINUE };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Unhealthy output detected
|
|
40
|
-
if (tracking.shouldEscalate) {
|
|
41
|
-
// Escalation threshold reached — inject recovery instruction
|
|
42
|
-
return {
|
|
43
|
-
result: HookResult.INJECT,
|
|
44
|
-
modifiedOutput: output,
|
|
45
|
-
injectRecovery: tracking.recoveryMessage ?? "recovery: compact context",
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// First anomaly (below threshold) — let it pass with a warning
|
|
50
|
-
return { result: HookResult.CONTINUE };
|
|
51
|
-
},
|
|
52
|
-
};
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// SubagentFailureHook — PostToolUse, priority=45, phase=LATE
|
|
3
|
-
//
|
|
4
|
-
// Mechanically tracks subagent task failures.
|
|
5
|
-
// At maxSubagentFailures consecutive failures on the same task,
|
|
6
|
-
// surfaces a BLOCKER.
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
import { HookPhase, HookResult } from "../types.ts";
|
|
10
|
-
import type { HookContext, PostToolUseHook } from "../types.ts";
|
|
11
|
-
import { DEFAULT_GUARD_CONFIG, checkGuardProgression } from "../../guards/guard-config.ts";
|
|
12
|
-
import type { GuardConfig } from "../../guards/guard-config.ts";
|
|
13
|
-
|
|
14
|
-
/** Module-level failure tracker — maps sessionId to consecutive failure count */
|
|
15
|
-
const failureCounters = new Map<string, number>();
|
|
16
|
-
|
|
17
|
-
export function resetSubagentFailures(sessionId?: string): void {
|
|
18
|
-
if (sessionId) {
|
|
19
|
-
failureCounters.delete(sessionId);
|
|
20
|
-
} else {
|
|
21
|
-
failureCounters.clear();
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function getSubagentFailureCount(sessionId: string): number {
|
|
26
|
-
return failureCounters.get(sessionId) ?? 0;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Error pattern detection — reuses the same patterns from error-recovery-hook
|
|
30
|
-
function outputLooksLikeFailure(output: string): boolean {
|
|
31
|
-
if (!output || output.length === 0) return true; // Empty output = failure
|
|
32
|
-
const errorPatterns = [
|
|
33
|
-
/error/i, /exception/i, /failed/i, /failure/i,
|
|
34
|
-
/unable to/i, /could not/i, /not found/i,
|
|
35
|
-
/ECONNREFUSED/i, /ETIMEDOUT/i, /rate.?limited/i,
|
|
36
|
-
/too many requests/i, /timeout/i, /execution.?timed.?out/i,
|
|
37
|
-
];
|
|
38
|
-
const head = output.slice(0, 2000);
|
|
39
|
-
return errorPatterns.some((p) => p.test(head));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export const subagentFailureHook: PostToolUseHook = {
|
|
43
|
-
metadata: {
|
|
44
|
-
name: "subagent-failure",
|
|
45
|
-
priority: 45,
|
|
46
|
-
phase: HookPhase.LATE,
|
|
47
|
-
dependencies: [],
|
|
48
|
-
errorHandling: "isolate",
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
async execute(context: HookContext, output: string) {
|
|
52
|
-
const sessionId = context.sessionId;
|
|
53
|
-
const config: GuardConfig = context._guardConfig ?? DEFAULT_GUARD_CONFIG;
|
|
54
|
-
const maxFailures = config.maxSubagentFailures;
|
|
55
|
-
|
|
56
|
-
if (maxFailures <= 0) {
|
|
57
|
-
// Disabled
|
|
58
|
-
return { result: HookResult.CONTINUE };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const isFailure = outputLooksLikeFailure(output);
|
|
62
|
-
let currentCount = failureCounters.get(sessionId) ?? 0;
|
|
63
|
-
|
|
64
|
-
if (isFailure) {
|
|
65
|
-
currentCount += 1;
|
|
66
|
-
failureCounters.set(sessionId, currentCount);
|
|
67
|
-
} else {
|
|
68
|
-
// Success — reset counter
|
|
69
|
-
failureCounters.set(sessionId, 0);
|
|
70
|
-
return { result: HookResult.CONTINUE };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Check progression
|
|
74
|
-
const progression = checkGuardProgression(currentCount, maxFailures, config);
|
|
75
|
-
|
|
76
|
-
if (progression.level === "stop") {
|
|
77
|
-
// Surface BLOCKER
|
|
78
|
-
return {
|
|
79
|
-
result: HookResult.INJECT,
|
|
80
|
-
modifiedOutput: output,
|
|
81
|
-
injectRecovery: `[HOOK: BLOCKER] ${currentCount} consecutive subagent failures (max ${maxFailures}). Surface to orchestrator with findings and stop delegating.`,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (progression.level === "warn" || progression.level === "escalate") {
|
|
86
|
-
// Annotate but don't stop
|
|
87
|
-
context._guardProgression = progression;
|
|
88
|
-
context._subagentFailures = currentCount;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return { result: HookResult.CONTINUE };
|
|
92
|
-
},
|
|
93
|
-
};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Memory module — barrel export
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
export {
|
|
6
|
-
MemoryLevel,
|
|
7
|
-
DEFAULT_BUDGETS,
|
|
8
|
-
} from "./interfaces.ts";
|
|
9
|
-
export type {
|
|
10
|
-
MemoryEntry,
|
|
11
|
-
MemorySnapshot,
|
|
12
|
-
MemoryConfig,
|
|
13
|
-
Finding,
|
|
14
|
-
Decision,
|
|
15
|
-
} from "./interfaces.ts";
|
|
16
|
-
|
|
17
|
-
export { MemoryManager } from "./memory-manager.ts";
|
|
18
|
-
export { PlanStore } from "./plan-store.ts";
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// 4-Tier Memory System — interfaces & types
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
export enum MemoryLevel {
|
|
6
|
-
SYSTEM = "system", // immutable OpenHermes identity, never pruned
|
|
7
|
-
PROJECT = "project", // project-level config, conventions, decisions
|
|
8
|
-
MISSION = "mission", // current session goal, active plan reference
|
|
9
|
-
TASK = "task", // per-step findings, cleared after each iteration
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface MemoryEntry {
|
|
13
|
-
id: string;
|
|
14
|
-
level: MemoryLevel;
|
|
15
|
-
content: string;
|
|
16
|
-
timestamp: number;
|
|
17
|
-
importance: number; // 0.0 to 1.0
|
|
18
|
-
metadata?: Record<string, string>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface MemorySnapshot {
|
|
22
|
-
system: MemoryEntry[];
|
|
23
|
-
project: MemoryEntry[];
|
|
24
|
-
mission: MemoryEntry[];
|
|
25
|
-
task: MemoryEntry[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface MemoryConfig {
|
|
29
|
-
budgets: Partial<Record<MemoryLevel, number>>; // max entries per level, defaults filled for missing
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface Finding {
|
|
33
|
-
id: string;
|
|
34
|
-
sessionId: string;
|
|
35
|
-
description: string;
|
|
36
|
-
severity: "info" | "warning" | "blocker";
|
|
37
|
-
timestamp: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface Decision {
|
|
41
|
-
id: string;
|
|
42
|
-
sessionId: string;
|
|
43
|
-
description: string;
|
|
44
|
-
rationale: string;
|
|
45
|
-
timestamp: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export const DEFAULT_BUDGETS: Record<MemoryLevel, number> = {
|
|
49
|
-
[MemoryLevel.SYSTEM]: 50,
|
|
50
|
-
[MemoryLevel.PROJECT]: 100,
|
|
51
|
-
[MemoryLevel.MISSION]: 30,
|
|
52
|
-
[MemoryLevel.TASK]: 20,
|
|
53
|
-
};
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// MemoryManager — singleton 4-tier hierarchical memory store
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
6
|
-
import {
|
|
7
|
-
MemoryLevel,
|
|
8
|
-
DEFAULT_BUDGETS,
|
|
9
|
-
} from "./interfaces.ts";
|
|
10
|
-
import type {
|
|
11
|
-
MemoryEntry,
|
|
12
|
-
MemorySnapshot,
|
|
13
|
-
MemoryConfig,
|
|
14
|
-
} from "./interfaces.ts";
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Manager
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
export class MemoryManager {
|
|
21
|
-
private static instance: MemoryManager | null = null;
|
|
22
|
-
|
|
23
|
-
private entries: Map<MemoryLevel, MemoryEntry[]> = new Map();
|
|
24
|
-
private config: MemoryConfig;
|
|
25
|
-
|
|
26
|
-
private constructor(config?: Partial<MemoryConfig>) {
|
|
27
|
-
this.config = {
|
|
28
|
-
budgets: { ...DEFAULT_BUDGETS, ...config?.budgets },
|
|
29
|
-
};
|
|
30
|
-
// Initialise every level so callers never hit undefined
|
|
31
|
-
for (const level of Object.values(MemoryLevel)) {
|
|
32
|
-
this.entries.set(level, []);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// -----------------------------------------------------------------------
|
|
37
|
-
// Singleton
|
|
38
|
-
// -----------------------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
static getInstance(config?: Partial<MemoryConfig>): MemoryManager {
|
|
41
|
-
if (!MemoryManager.instance) {
|
|
42
|
-
MemoryManager.instance = new MemoryManager(config);
|
|
43
|
-
}
|
|
44
|
-
return MemoryManager.instance;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Reset singleton — used in tests for isolation. */
|
|
48
|
-
static resetInstance(): void {
|
|
49
|
-
MemoryManager.instance = null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// -----------------------------------------------------------------------
|
|
53
|
-
// Public API
|
|
54
|
-
// -----------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Add a memory entry at the given level.
|
|
58
|
-
* Inserts, sorts by importance DESC then timestamp DESC, then prunes.
|
|
59
|
-
*/
|
|
60
|
-
add(
|
|
61
|
-
level: MemoryLevel,
|
|
62
|
-
content: string,
|
|
63
|
-
importance: number,
|
|
64
|
-
metadata?: Record<string, string>,
|
|
65
|
-
): MemoryEntry {
|
|
66
|
-
const entry: MemoryEntry = {
|
|
67
|
-
id: randomUUID(),
|
|
68
|
-
level,
|
|
69
|
-
content,
|
|
70
|
-
timestamp: Date.now(),
|
|
71
|
-
importance: Math.max(0, Math.min(1, importance)), // clamp [0, 1]
|
|
72
|
-
metadata,
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const bucket = this.entries.get(level)!;
|
|
76
|
-
bucket.push(entry);
|
|
77
|
-
|
|
78
|
-
// Sort: importance DESC, then timestamp DESC (newer first for ties)
|
|
79
|
-
bucket.sort((a, b) => {
|
|
80
|
-
if (b.importance !== a.importance) return b.importance - a.importance;
|
|
81
|
-
return b.timestamp - a.timestamp;
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
this.prune(level);
|
|
85
|
-
return entry;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Return a formatted context string for all levels.
|
|
90
|
-
* If a query string is provided, only entries whose content includes
|
|
91
|
-
* the query (case-insensitive) are returned.
|
|
92
|
-
*/
|
|
93
|
-
getContext(query?: string): string {
|
|
94
|
-
const parts: string[] = [];
|
|
95
|
-
|
|
96
|
-
for (const level of Object.values(MemoryLevel)) {
|
|
97
|
-
const bucket = this.entries.get(level) ?? [];
|
|
98
|
-
let filtered = bucket;
|
|
99
|
-
|
|
100
|
-
if (query && query.length > 0) {
|
|
101
|
-
const q = query.toLowerCase();
|
|
102
|
-
filtered = bucket.filter((e) => e.content.toLowerCase().includes(q));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (filtered.length === 0) continue;
|
|
106
|
-
|
|
107
|
-
const heading = level.toUpperCase();
|
|
108
|
-
const lines = filtered.map((e) => {
|
|
109
|
-
const imp = e.importance.toFixed(2);
|
|
110
|
-
const meta = e.metadata ? ` [${formatMetadata(e.metadata)}]` : "";
|
|
111
|
-
return ` [${imp}] ${e.content}${meta}`;
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
parts.push(`== ${heading} ==\n${lines.join("\n")}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return parts.join("\n\n");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Remove the least important entries from a level when budget is exceeded.
|
|
122
|
-
* Falls back to DEFAULT_BUDGETS if no explicit budget is configured.
|
|
123
|
-
*/
|
|
124
|
-
prune(level: MemoryLevel): void {
|
|
125
|
-
const budget = this.config.budgets[level] ?? DEFAULT_BUDGETS[level];
|
|
126
|
-
|
|
127
|
-
// Guard: budget must be a valid non-negative number
|
|
128
|
-
if (typeof budget !== "number" || budget < 0 || !Number.isFinite(budget)) {
|
|
129
|
-
console.warn(
|
|
130
|
-
`[MemoryManager] Invalid budget for level "${level}": ${budget}. Skipping prune.`,
|
|
131
|
-
);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const bucket = this.entries.get(level);
|
|
136
|
-
if (!bucket) return;
|
|
137
|
-
if (bucket.length <= budget) return;
|
|
138
|
-
|
|
139
|
-
// Already sorted: importance DESC → drop from the end
|
|
140
|
-
bucket.splice(budget);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Clear all entries at a given level (used for TASK after iteration). */
|
|
144
|
-
clearLevel(level: MemoryLevel): void {
|
|
145
|
-
this.entries.set(level, []);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Serialise all entries into a snapshot. */
|
|
149
|
-
export(): MemorySnapshot {
|
|
150
|
-
return {
|
|
151
|
-
system: [...(this.entries.get(MemoryLevel.SYSTEM) ?? [])],
|
|
152
|
-
project: [...(this.entries.get(MemoryLevel.PROJECT) ?? [])],
|
|
153
|
-
mission: [...(this.entries.get(MemoryLevel.MISSION) ?? [])],
|
|
154
|
-
task: [...(this.entries.get(MemoryLevel.TASK) ?? [])],
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Restore state from a snapshot. */
|
|
159
|
-
import(snapshot: MemorySnapshot): void {
|
|
160
|
-
this.entries.set(MemoryLevel.SYSTEM, [...snapshot.system]);
|
|
161
|
-
this.entries.set(MemoryLevel.PROJECT, [...snapshot.project]);
|
|
162
|
-
this.entries.set(MemoryLevel.MISSION, [...snapshot.mission]);
|
|
163
|
-
this.entries.set(MemoryLevel.TASK, [...snapshot.task]);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Get entries, optionally filtered by level. */
|
|
167
|
-
getEntries(level?: MemoryLevel): MemoryEntry[] {
|
|
168
|
-
if (level) {
|
|
169
|
-
return [...(this.entries.get(level) ?? [])];
|
|
170
|
-
}
|
|
171
|
-
const all: MemoryEntry[] = [];
|
|
172
|
-
for (const lvl of Object.values(MemoryLevel)) {
|
|
173
|
-
all.push(...(this.entries.get(lvl) ?? []));
|
|
174
|
-
}
|
|
175
|
-
return all;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** Count entries at a given level. */
|
|
179
|
-
getEntryCount(level: MemoryLevel): number {
|
|
180
|
-
return this.entries.get(level)?.length ?? 0;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Update the budgets after construction. */
|
|
184
|
-
setBudgets(budgets: Partial<Record<MemoryLevel, number>>): void {
|
|
185
|
-
for (const [level, budget] of Object.entries(budgets)) {
|
|
186
|
-
if (budget !== undefined && Object.values(MemoryLevel).includes(level as MemoryLevel)) {
|
|
187
|
-
this.config.budgets[level as MemoryLevel] = budget;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
// Re-prune all levels with new budgets
|
|
191
|
-
for (const level of Object.values(MemoryLevel)) {
|
|
192
|
-
this.prune(level);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// ---------------------------------------------------------------------------
|
|
198
|
-
// Helpers
|
|
199
|
-
// ---------------------------------------------------------------------------
|
|
200
|
-
|
|
201
|
-
function formatMetadata(meta: Record<string, string>): string {
|
|
202
|
-
return Object.entries(meta)
|
|
203
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
204
|
-
.join(", ");
|
|
205
|
-
}
|