@danielsimonjr/memoryjs 1.9.0 → 1.15.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 +513 -148
- package/dist/cli/index.js +3621 -294
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +12465 -2481
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2766 -306
- package/dist/index.d.ts +2766 -306
- package/dist/index.js +21441 -11464
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/cli/index.js
CHANGED
|
@@ -495,7 +495,7 @@ var init_indexes = __esm({
|
|
|
495
495
|
});
|
|
496
496
|
|
|
497
497
|
// src/utils/errors.ts
|
|
498
|
-
var KnowledgeGraphError, EntityNotFoundError, RelationNotFoundError, ValidationError, CycleDetectedError, InvalidImportanceError, FileOperationError, InsufficientEntitiesError, RefConflictError, LowEntropyContentError, OperationCancelledError;
|
|
498
|
+
var KnowledgeGraphError, EntityNotFoundError, RelationNotFoundError, ValidationError, CycleDetectedError, InvalidImportanceError, FileOperationError, InsufficientEntitiesError, RefConflictError, VersionConflictError, LowEntropyContentError, OperationCancelledError;
|
|
499
499
|
var init_errors = __esm({
|
|
500
500
|
"src/utils/errors.ts"() {
|
|
501
501
|
"use strict";
|
|
@@ -662,6 +662,31 @@ ${this.suggestions.map((s) => ` - ${s}`).join("\n")}`;
|
|
|
662
662
|
this.name = "RefConflictError";
|
|
663
663
|
}
|
|
664
664
|
};
|
|
665
|
+
VersionConflictError = class extends KnowledgeGraphError {
|
|
666
|
+
entityName;
|
|
667
|
+
expected;
|
|
668
|
+
actual;
|
|
669
|
+
conflictingAgentId;
|
|
670
|
+
constructor(entityName, expected, actual, conflictingAgentId) {
|
|
671
|
+
super(
|
|
672
|
+
`Version conflict on entity '${entityName}': expected v${expected}, found v${actual}`,
|
|
673
|
+
"VERSION_CONFLICT",
|
|
674
|
+
{
|
|
675
|
+
context: { entityName, expected, actual, conflictingAgentId },
|
|
676
|
+
suggestions: [
|
|
677
|
+
"Re-fetch the entity to get the current version",
|
|
678
|
+
"Reconcile your changes with the live state",
|
|
679
|
+
"Retry the update with the new expectedVersion"
|
|
680
|
+
]
|
|
681
|
+
}
|
|
682
|
+
);
|
|
683
|
+
this.name = "VersionConflictError";
|
|
684
|
+
this.entityName = entityName;
|
|
685
|
+
this.expected = expected;
|
|
686
|
+
this.actual = actual;
|
|
687
|
+
this.conflictingAgentId = conflictingAgentId;
|
|
688
|
+
}
|
|
689
|
+
};
|
|
665
690
|
LowEntropyContentError = class extends Error {
|
|
666
691
|
/** Stable error code for programmatic handling */
|
|
667
692
|
code = "LOW_ENTROPY_CONTENT";
|
|
@@ -1006,7 +1031,8 @@ var init_schemas = __esm({
|
|
|
1006
1031
|
lastModified: isoDateSchema.optional(),
|
|
1007
1032
|
tags: z.array(tagSchema).optional(),
|
|
1008
1033
|
importance: importanceSchema.optional(),
|
|
1009
|
-
parentId: entityNameSchema.optional()
|
|
1034
|
+
parentId: entityNameSchema.optional(),
|
|
1035
|
+
contentHash: z.string().length(64).optional()
|
|
1010
1036
|
}).strict();
|
|
1011
1037
|
CreateEntitySchema = z.object({
|
|
1012
1038
|
name: entityNameSchema,
|
|
@@ -1018,12 +1044,26 @@ var init_schemas = __esm({
|
|
|
1018
1044
|
projectId: z.string().optional(),
|
|
1019
1045
|
createdAt: isoDateSchema.optional(),
|
|
1020
1046
|
lastModified: isoDateSchema.optional(),
|
|
1047
|
+
// v1.6.0: Freshness
|
|
1048
|
+
ttl: z.number().optional(),
|
|
1049
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
1021
1050
|
// v1.8.0: Memory versioning fields
|
|
1022
1051
|
version: z.number().int().positive().optional(),
|
|
1023
1052
|
parentEntityName: z.string().optional(),
|
|
1024
1053
|
rootEntityName: z.string().optional(),
|
|
1025
1054
|
isLatest: z.boolean().optional(),
|
|
1026
|
-
supersededBy: z.string().optional()
|
|
1055
|
+
supersededBy: z.string().optional(),
|
|
1056
|
+
// v1.11.0: Memory Engine dedup
|
|
1057
|
+
contentHash: z.string().length(64).optional(),
|
|
1058
|
+
// η.4.4: Bitemporal validity
|
|
1059
|
+
validFrom: z.string().optional(),
|
|
1060
|
+
validUntil: z.string().optional(),
|
|
1061
|
+
observationMeta: z.array(z.object({
|
|
1062
|
+
content: z.string(),
|
|
1063
|
+
validFrom: z.string().optional(),
|
|
1064
|
+
validUntil: z.string().optional(),
|
|
1065
|
+
recordedAt: z.string().optional()
|
|
1066
|
+
})).optional()
|
|
1027
1067
|
}).strict();
|
|
1028
1068
|
UpdateEntitySchema = z.object({
|
|
1029
1069
|
entityType: entityTypeSchema.optional(),
|
|
@@ -1033,7 +1073,21 @@ var init_schemas = __esm({
|
|
|
1033
1073
|
parentId: entityNameSchema.optional(),
|
|
1034
1074
|
// v1.8.0: Memory versioning fields
|
|
1035
1075
|
isLatest: z.boolean().optional(),
|
|
1036
|
-
supersededBy: z.string().optional()
|
|
1076
|
+
supersededBy: z.string().optional(),
|
|
1077
|
+
rootEntityName: entityNameSchema.optional(),
|
|
1078
|
+
parentEntityName: entityNameSchema.optional(),
|
|
1079
|
+
version: z.number().int().min(1).optional(),
|
|
1080
|
+
// v1.11.0: Memory Engine dedup
|
|
1081
|
+
contentHash: z.string().length(64).optional(),
|
|
1082
|
+
// η.4.4: Temporal Versioning expansion
|
|
1083
|
+
validFrom: z.string().optional(),
|
|
1084
|
+
validUntil: z.string().optional(),
|
|
1085
|
+
observationMeta: z.array(z.object({
|
|
1086
|
+
content: z.string(),
|
|
1087
|
+
validFrom: z.string().optional(),
|
|
1088
|
+
validUntil: z.string().optional(),
|
|
1089
|
+
recordedAt: z.string().optional()
|
|
1090
|
+
})).optional()
|
|
1037
1091
|
}).strict();
|
|
1038
1092
|
RelationSchema = z.object({
|
|
1039
1093
|
from: entityNameSchema,
|
|
@@ -1107,7 +1161,18 @@ var init_schemas = __esm({
|
|
|
1107
1161
|
entityType: entityTypeSchema.optional()
|
|
1108
1162
|
}).strict();
|
|
1109
1163
|
ImportFormatSchema = z.enum(["json", "csv", "graphml"]);
|
|
1110
|
-
ExtendedExportFormatSchema = z.enum([
|
|
1164
|
+
ExtendedExportFormatSchema = z.enum([
|
|
1165
|
+
"json",
|
|
1166
|
+
"csv",
|
|
1167
|
+
"graphml",
|
|
1168
|
+
"gexf",
|
|
1169
|
+
"dot",
|
|
1170
|
+
"markdown",
|
|
1171
|
+
"mermaid",
|
|
1172
|
+
"turtle",
|
|
1173
|
+
"rdf-xml",
|
|
1174
|
+
"json-ld"
|
|
1175
|
+
]);
|
|
1111
1176
|
MergeStrategySchema = z.enum(["replace", "skip", "merge", "fail"]);
|
|
1112
1177
|
ExportFilterSchema = z.object({
|
|
1113
1178
|
startDate: isoDateSchema.optional(),
|
|
@@ -1251,17 +1316,20 @@ function escapeCsvFormula(field) {
|
|
|
1251
1316
|
}
|
|
1252
1317
|
return str;
|
|
1253
1318
|
}
|
|
1254
|
-
function validateFilePath(filePath, baseDir = process.cwd(), confineToBase =
|
|
1255
|
-
|
|
1256
|
-
const absolute = path2.isAbsolute(normalized) ? normalized : path2.join(baseDir, normalized);
|
|
1257
|
-
const finalNormalized = path2.normalize(absolute);
|
|
1258
|
-
const segments = finalNormalized.split(path2.sep);
|
|
1259
|
-
if (segments.includes("..")) {
|
|
1319
|
+
function validateFilePath(filePath, baseDir = process.cwd(), confineToBase = true) {
|
|
1320
|
+
if (filePath.split(/[/\\]/).includes("..")) {
|
|
1260
1321
|
throw new FileOperationError(
|
|
1261
1322
|
`Path traversal detected in file path: ${filePath}`,
|
|
1262
1323
|
filePath
|
|
1263
1324
|
);
|
|
1264
1325
|
}
|
|
1326
|
+
const finalNormalized = path2.resolve(baseDir, filePath);
|
|
1327
|
+
if (finalNormalized.split(path2.sep).includes("..")) {
|
|
1328
|
+
throw new FileOperationError(
|
|
1329
|
+
`Path traversal detected in file path after resolution: ${filePath}`,
|
|
1330
|
+
filePath
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1265
1333
|
if (confineToBase) {
|
|
1266
1334
|
const resolvedBase = path2.resolve(baseDir) + path2.sep;
|
|
1267
1335
|
if (!finalNormalized.startsWith(resolvedBase) && finalNormalized !== resolvedBase.slice(0, -1)) {
|
|
@@ -1831,7 +1899,7 @@ var init_StreamingExporter = __esm({
|
|
|
1831
1899
|
* @throws {FileOperationError} If path traversal is detected
|
|
1832
1900
|
*/
|
|
1833
1901
|
constructor(filePath) {
|
|
1834
|
-
this.validatedFilePath = validateFilePath(filePath);
|
|
1902
|
+
this.validatedFilePath = validateFilePath(filePath, void 0, false);
|
|
1835
1903
|
}
|
|
1836
1904
|
/**
|
|
1837
1905
|
* Get the validated file path.
|
|
@@ -2434,7 +2502,26 @@ var init_EntityManager = __esm({
|
|
|
2434
2502
|
* }
|
|
2435
2503
|
* ```
|
|
2436
2504
|
*/
|
|
2437
|
-
|
|
2505
|
+
/**
|
|
2506
|
+
* Update an entity with optional optimistic-concurrency-control (η.5.5.c).
|
|
2507
|
+
*
|
|
2508
|
+
* Pass `options.expectedVersion` to enforce OCC: the caller asserts the
|
|
2509
|
+
* live entity has a specific `version`. If it differs (because another
|
|
2510
|
+
* agent / consolidation pass / contradiction-resolution incremented it
|
|
2511
|
+
* since the caller fetched), `VersionConflictError` is thrown with the
|
|
2512
|
+
* expected and actual versions. Omit `expectedVersion` for legacy
|
|
2513
|
+
* last-write-wins semantics (the default — backwards-compat).
|
|
2514
|
+
*
|
|
2515
|
+
* On a successful OCC-guarded write, `version` is auto-incremented:
|
|
2516
|
+
* `(entity.version ?? 1) + 1`. This makes OCC composable with the
|
|
2517
|
+
* existing v1.8.0 supersession-driven version increments.
|
|
2518
|
+
*
|
|
2519
|
+
* **Caveat**: a `ConsolidationScheduler` running in the background can
|
|
2520
|
+
* increment `version` between caller fetch and update, producing
|
|
2521
|
+
* spurious conflicts. Don't cache `expectedVersion` across scheduler
|
|
2522
|
+
* cycles — fetch immediately before writing.
|
|
2523
|
+
*/
|
|
2524
|
+
async updateEntity(name, updates, options) {
|
|
2438
2525
|
const validation = UpdateEntitySchema.safeParse(updates);
|
|
2439
2526
|
if (!validation.success) {
|
|
2440
2527
|
const errors = validation.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`);
|
|
@@ -2447,8 +2534,17 @@ var init_EntityManager = __esm({
|
|
|
2447
2534
|
if (!entity) {
|
|
2448
2535
|
throw new EntityNotFoundError(name);
|
|
2449
2536
|
}
|
|
2537
|
+
if (options?.expectedVersion !== void 0) {
|
|
2538
|
+
const liveVersion = entity.version ?? 1;
|
|
2539
|
+
if (liveVersion !== options.expectedVersion) {
|
|
2540
|
+
throw new VersionConflictError(name, options.expectedVersion, liveVersion);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2450
2543
|
Object.assign(entity, sanitizeObject(updates));
|
|
2451
2544
|
entity.lastModified = (/* @__PURE__ */ new Date()).toISOString();
|
|
2545
|
+
if (options?.expectedVersion !== void 0) {
|
|
2546
|
+
entity.version = (entity.version ?? 1) + 1;
|
|
2547
|
+
}
|
|
2452
2548
|
await this.storage.saveGraph(graph);
|
|
2453
2549
|
return entity;
|
|
2454
2550
|
} finally {
|
|
@@ -2708,6 +2804,85 @@ var init_EntityManager = __esm({
|
|
|
2708
2804
|
release();
|
|
2709
2805
|
}
|
|
2710
2806
|
}
|
|
2807
|
+
// ==================== η.4.4: Temporal Versioning ====================
|
|
2808
|
+
//
|
|
2809
|
+
// Mirrors the v1.9.0 RelationManager surface (invalidateRelation /
|
|
2810
|
+
// queryAsOf / timeline) for entities. Orthogonal to v1.8.0 supersession
|
|
2811
|
+
// (`version`/`supersededBy`): supersession answers "which version is
|
|
2812
|
+
// current?", temporal validity answers "was the entity true at time T?".
|
|
2813
|
+
// An entity may be superseded but still valid at a past asOf date, and
|
|
2814
|
+
// vice versa.
|
|
2815
|
+
/**
|
|
2816
|
+
* Mark an entity as no longer valid by setting `validUntil`. Idempotent:
|
|
2817
|
+
* a second call updates the existing `validUntil`. Does not delete the
|
|
2818
|
+
* entity — `entityAsOf` still returns it for past asOf timestamps.
|
|
2819
|
+
*
|
|
2820
|
+
* @param name - The entity to invalidate
|
|
2821
|
+
* @param ended - ISO 8601 timestamp; defaults to current time
|
|
2822
|
+
* @throws {EntityNotFoundError} If no entity exists with the given name
|
|
2823
|
+
*/
|
|
2824
|
+
async invalidateEntity(name, ended) {
|
|
2825
|
+
const release = await this.storage.graphMutex.acquire();
|
|
2826
|
+
try {
|
|
2827
|
+
const graph = await this.storage.getGraphForMutation();
|
|
2828
|
+
const entity = graph.entities.find((e) => e.name === name);
|
|
2829
|
+
if (!entity) throw new EntityNotFoundError(name);
|
|
2830
|
+
entity.validUntil = ended ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2831
|
+
entity.lastModified = (/* @__PURE__ */ new Date()).toISOString();
|
|
2832
|
+
await this.storage.saveGraph(graph);
|
|
2833
|
+
} finally {
|
|
2834
|
+
release();
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* Return the entity at a given point in time, or null if it didn't exist
|
|
2839
|
+
* (or was already invalidated) then. An entity is valid at `asOf` when:
|
|
2840
|
+
* - `validFrom` is undefined OR `validFrom` <= asOf
|
|
2841
|
+
* - `validUntil` is undefined OR `validUntil` >= asOf
|
|
2842
|
+
*
|
|
2843
|
+
* @param name - The entity name
|
|
2844
|
+
* @param asOf - ISO 8601 date string
|
|
2845
|
+
* @throws {ValidationError} If `asOf` is not an ISO 8601 date string
|
|
2846
|
+
*/
|
|
2847
|
+
async entityAsOf(name, asOf) {
|
|
2848
|
+
if (!/^\d{4}-\d{2}-\d{2}/.test(asOf)) {
|
|
2849
|
+
throw new ValidationError(`asOf must be an ISO 8601 date string, got: '${asOf}'`, []);
|
|
2850
|
+
}
|
|
2851
|
+
const graph = await this.storage.loadGraph();
|
|
2852
|
+
const entity = graph.entities.find((e) => e.name === name);
|
|
2853
|
+
if (!entity) return null;
|
|
2854
|
+
const vf = entity.validFrom;
|
|
2855
|
+
const vu = entity.validUntil;
|
|
2856
|
+
if (vf && vf > asOf) return null;
|
|
2857
|
+
if (vu && vu < asOf) return null;
|
|
2858
|
+
return entity;
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* Return all temporal versions of an entity in chronological order
|
|
2862
|
+
* (by `validFrom`, with unbounded entities last). When `name` matches
|
|
2863
|
+
* a member of a v1.8.0 supersession chain, returns the full chain
|
|
2864
|
+
* sorted by `validFrom`. Otherwise returns just the named entity (or []).
|
|
2865
|
+
*
|
|
2866
|
+
* @param name - Any entity name in the chain
|
|
2867
|
+
*/
|
|
2868
|
+
async entityTimeline(name) {
|
|
2869
|
+
const graph = await this.storage.loadGraph();
|
|
2870
|
+
const entity = graph.entities.find((e) => e.name === name);
|
|
2871
|
+
if (!entity) return [];
|
|
2872
|
+
const rootName = entity.rootEntityName ?? entity.name;
|
|
2873
|
+
const chain = graph.entities.filter(
|
|
2874
|
+
(e) => (e.rootEntityName ?? e.name) === rootName
|
|
2875
|
+
);
|
|
2876
|
+
chain.sort((a, b) => {
|
|
2877
|
+
const aFrom = a.validFrom ?? "";
|
|
2878
|
+
const bFrom = b.validFrom ?? "";
|
|
2879
|
+
if (!aFrom && !bFrom) return 0;
|
|
2880
|
+
if (!aFrom) return 1;
|
|
2881
|
+
if (!bFrom) return -1;
|
|
2882
|
+
return aFrom.localeCompare(bFrom);
|
|
2883
|
+
});
|
|
2884
|
+
return chain;
|
|
2885
|
+
}
|
|
2711
2886
|
};
|
|
2712
2887
|
}
|
|
2713
2888
|
});
|
|
@@ -2752,10 +2927,177 @@ var init_IOManager = __esm({
|
|
|
2752
2927
|
return this.exportAsMarkdown(graph);
|
|
2753
2928
|
case "mermaid":
|
|
2754
2929
|
return this.exportAsMermaid(graph);
|
|
2930
|
+
case "turtle":
|
|
2931
|
+
return this.exportAsTurtle(graph);
|
|
2932
|
+
case "rdf-xml":
|
|
2933
|
+
return this.exportAsRdfXml(graph);
|
|
2934
|
+
case "json-ld":
|
|
2935
|
+
return this.exportAsJsonLd(graph);
|
|
2755
2936
|
default:
|
|
2756
2937
|
throw new Error(`Unsupported export format: ${format}`);
|
|
2757
2938
|
}
|
|
2758
2939
|
}
|
|
2940
|
+
// -------- η.5.4 Standards Compliance — RDF / Turtle / JSON-LD --------
|
|
2941
|
+
/** IRI for an entity resource. Format: `urn:memoryjs:entity:<percent-encoded-name>`. */
|
|
2942
|
+
entityIri(name) {
|
|
2943
|
+
return `urn:memoryjs:entity:${encodeURIComponent(name)}`;
|
|
2944
|
+
}
|
|
2945
|
+
/** IRI for a relation predicate. Format: `urn:memoryjs:rel:<percent-encoded-type>`. */
|
|
2946
|
+
relationIri(type) {
|
|
2947
|
+
return `urn:memoryjs:rel:${encodeURIComponent(type)}`;
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* Escape a string for a Turtle `STRING_LITERAL_QUOTE` per W3C Turtle 1.1.
|
|
2951
|
+
* - Named ECHAR escapes for `\\ " \n \r \t \b \f`
|
|
2952
|
+
* - Other C0 control chars (forbidden unescaped in `"..."`) as `\uXXXX`
|
|
2953
|
+
*/
|
|
2954
|
+
turtleEscape(s) {
|
|
2955
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/\x08/g, "\\b").replace(/\x0c/g, "\\f").replace(
|
|
2956
|
+
/[\x00-\x07\x0B\x0E-\x1F]/g,
|
|
2957
|
+
(c) => `\\u${c.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}`
|
|
2958
|
+
);
|
|
2959
|
+
}
|
|
2960
|
+
/**
|
|
2961
|
+
* Test whether a string is a valid XML 1.0 NCName — ASCII subset,
|
|
2962
|
+
* sufficient because `relationIri()` percent-encodes everything else.
|
|
2963
|
+
* RDF/XML requires this for property-element predicate names.
|
|
2964
|
+
*/
|
|
2965
|
+
isValidNCName(s) {
|
|
2966
|
+
return /^[A-Za-z_][A-Za-z0-9_\-.]*$/.test(s);
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* Export as Turtle (W3C RDF 1.1).
|
|
2970
|
+
* - entity → `urn:memoryjs:entity:<name>` resource
|
|
2971
|
+
* - entityType → `rdf:type` with `urn:memoryjs:type:<type>` IRI
|
|
2972
|
+
* - observations → `rdfs:comment` literals
|
|
2973
|
+
* - tags → `dcterms:subject` literals
|
|
2974
|
+
* - createdAt → `dcterms:created` literal
|
|
2975
|
+
* - relation → `<from> <urn:memoryjs:rel:<type>> <to>` triple
|
|
2976
|
+
*/
|
|
2977
|
+
exportAsTurtle(graph) {
|
|
2978
|
+
const lines = [];
|
|
2979
|
+
lines.push("@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .");
|
|
2980
|
+
lines.push("@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .");
|
|
2981
|
+
lines.push("@prefix dcterms: <http://purl.org/dc/terms/> .");
|
|
2982
|
+
lines.push("");
|
|
2983
|
+
for (const entity of graph.entities) {
|
|
2984
|
+
const subject = `<${this.entityIri(entity.name)}>`;
|
|
2985
|
+
lines.push(`${subject} a <urn:memoryjs:type:${encodeURIComponent(entity.entityType)}> ;`);
|
|
2986
|
+
lines.push(` rdfs:label "${this.turtleEscape(entity.name)}" ;`);
|
|
2987
|
+
for (const obs of entity.observations) {
|
|
2988
|
+
lines.push(` rdfs:comment "${this.turtleEscape(obs)}" ;`);
|
|
2989
|
+
}
|
|
2990
|
+
for (const tag of entity.tags ?? []) {
|
|
2991
|
+
lines.push(` dcterms:subject "${this.turtleEscape(tag)}" ;`);
|
|
2992
|
+
}
|
|
2993
|
+
if (entity.createdAt) {
|
|
2994
|
+
lines.push(` dcterms:created "${this.turtleEscape(entity.createdAt)}" ;`);
|
|
2995
|
+
}
|
|
2996
|
+
const last = lines.length - 1;
|
|
2997
|
+
lines[last] = lines[last].replace(/ ;$/, " .");
|
|
2998
|
+
}
|
|
2999
|
+
if (graph.entities.length > 0) lines.push("");
|
|
3000
|
+
for (const relation of graph.relations) {
|
|
3001
|
+
const subject = `<${this.entityIri(relation.from)}>`;
|
|
3002
|
+
const predicate = `<${this.relationIri(relation.relationType)}>`;
|
|
3003
|
+
const object = `<${this.entityIri(relation.to)}>`;
|
|
3004
|
+
lines.push(`${subject} ${predicate} ${object} .`);
|
|
3005
|
+
}
|
|
3006
|
+
return lines.join("\n");
|
|
3007
|
+
}
|
|
3008
|
+
/**
|
|
3009
|
+
* Export as RDF/XML (W3C RDF 1.1 XML serialization).
|
|
3010
|
+
* - Same triple set as Turtle, in XML form
|
|
3011
|
+
* - NCName-valid relation types → property-element under `mjsRel:`
|
|
3012
|
+
* - Otherwise → asserted `mjsRel:link` triple plus `rdf:Statement` reification preserving the original predicate IRI
|
|
3013
|
+
*/
|
|
3014
|
+
exportAsRdfXml(graph) {
|
|
3015
|
+
const xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3016
|
+
const lines = [];
|
|
3017
|
+
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
3018
|
+
lines.push("<rdf:RDF");
|
|
3019
|
+
lines.push(' xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"');
|
|
3020
|
+
lines.push(' xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"');
|
|
3021
|
+
lines.push(' xmlns:dcterms="http://purl.org/dc/terms/"');
|
|
3022
|
+
lines.push(' xmlns:mjsRel="urn:memoryjs:rel:">');
|
|
3023
|
+
for (const entity of graph.entities) {
|
|
3024
|
+
lines.push(` <rdf:Description rdf:about="${xmlEscape(this.entityIri(entity.name))}">`);
|
|
3025
|
+
lines.push(` <rdf:type rdf:resource="urn:memoryjs:type:${xmlEscape(encodeURIComponent(entity.entityType))}"/>`);
|
|
3026
|
+
lines.push(` <rdfs:label>${xmlEscape(entity.name)}</rdfs:label>`);
|
|
3027
|
+
for (const obs of entity.observations) {
|
|
3028
|
+
lines.push(` <rdfs:comment>${xmlEscape(obs)}</rdfs:comment>`);
|
|
3029
|
+
}
|
|
3030
|
+
for (const tag of entity.tags ?? []) {
|
|
3031
|
+
lines.push(` <dcterms:subject>${xmlEscape(tag)}</dcterms:subject>`);
|
|
3032
|
+
}
|
|
3033
|
+
lines.push(" </rdf:Description>");
|
|
3034
|
+
}
|
|
3035
|
+
for (const relation of graph.relations) {
|
|
3036
|
+
const fromIri = xmlEscape(this.entityIri(relation.from));
|
|
3037
|
+
const toIri = xmlEscape(this.entityIri(relation.to));
|
|
3038
|
+
if (this.isValidNCName(relation.relationType)) {
|
|
3039
|
+
lines.push(` <rdf:Description rdf:about="${fromIri}">`);
|
|
3040
|
+
lines.push(` <mjsRel:${xmlEscape(relation.relationType)} rdf:resource="${toIri}"/>`);
|
|
3041
|
+
lines.push(" </rdf:Description>");
|
|
3042
|
+
continue;
|
|
3043
|
+
}
|
|
3044
|
+
const predIri = xmlEscape(this.relationIri(relation.relationType));
|
|
3045
|
+
lines.push(` <rdf:Description rdf:about="${fromIri}">`);
|
|
3046
|
+
lines.push(` <mjsRel:link rdf:resource="${toIri}"/>`);
|
|
3047
|
+
lines.push(" </rdf:Description>");
|
|
3048
|
+
lines.push(" <rdf:Description>");
|
|
3049
|
+
lines.push(' <rdf:type rdf:resource="http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement"/>');
|
|
3050
|
+
lines.push(` <rdf:subject rdf:resource="${fromIri}"/>`);
|
|
3051
|
+
lines.push(` <rdf:predicate rdf:resource="${predIri}"/>`);
|
|
3052
|
+
lines.push(` <rdf:object rdf:resource="${toIri}"/>`);
|
|
3053
|
+
lines.push(" </rdf:Description>");
|
|
3054
|
+
}
|
|
3055
|
+
lines.push("</rdf:RDF>");
|
|
3056
|
+
return lines.join("\n");
|
|
3057
|
+
}
|
|
3058
|
+
/**
|
|
3059
|
+
* Export as JSON-LD (JSON for Linking Data).
|
|
3060
|
+
* - `@context` maps memoryjs schema to RDFS + DCTerms vocabularies
|
|
3061
|
+
* - observations/tags use `@container: @set` so each value becomes its own triple (matches Turtle/RDF-XML), not an `rdf:List`
|
|
3062
|
+
* - any JSON-LD parser yields the same RDF graph as the Turtle export
|
|
3063
|
+
*/
|
|
3064
|
+
exportAsJsonLd(graph) {
|
|
3065
|
+
const context = {
|
|
3066
|
+
"@vocab": "urn:memoryjs:",
|
|
3067
|
+
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
|
|
3068
|
+
dcterms: "http://purl.org/dc/terms/",
|
|
3069
|
+
name: "rdfs:label",
|
|
3070
|
+
entityType: "@type",
|
|
3071
|
+
observations: { "@id": "rdfs:comment", "@container": "@set" },
|
|
3072
|
+
tags: { "@id": "dcterms:subject", "@container": "@set" },
|
|
3073
|
+
createdAt: "dcterms:created",
|
|
3074
|
+
lastModified: "dcterms:modified",
|
|
3075
|
+
from: { "@id": "urn:memoryjs:rel:from", "@type": "@id" },
|
|
3076
|
+
to: { "@id": "urn:memoryjs:rel:to", "@type": "@id" },
|
|
3077
|
+
relationType: "urn:memoryjs:rel:type"
|
|
3078
|
+
};
|
|
3079
|
+
const doc = {
|
|
3080
|
+
"@context": context,
|
|
3081
|
+
"@graph": [
|
|
3082
|
+
...graph.entities.map((entity) => ({
|
|
3083
|
+
"@id": this.entityIri(entity.name),
|
|
3084
|
+
name: entity.name,
|
|
3085
|
+
entityType: `urn:memoryjs:type:${encodeURIComponent(entity.entityType)}`,
|
|
3086
|
+
observations: entity.observations,
|
|
3087
|
+
...entity.tags && entity.tags.length > 0 ? { tags: entity.tags } : {},
|
|
3088
|
+
...entity.createdAt ? { createdAt: entity.createdAt } : {},
|
|
3089
|
+
...entity.lastModified ? { lastModified: entity.lastModified } : {}
|
|
3090
|
+
})),
|
|
3091
|
+
...graph.relations.map((relation) => ({
|
|
3092
|
+
"@id": `urn:memoryjs:relation:${encodeURIComponent(relation.from)}:${encodeURIComponent(relation.relationType)}:${encodeURIComponent(relation.to)}`,
|
|
3093
|
+
from: this.entityIri(relation.from),
|
|
3094
|
+
to: this.entityIri(relation.to),
|
|
3095
|
+
relationType: relation.relationType
|
|
3096
|
+
}))
|
|
3097
|
+
]
|
|
3098
|
+
};
|
|
3099
|
+
return JSON.stringify(doc, null, 2);
|
|
3100
|
+
}
|
|
2759
3101
|
/** Export graph with optional brotli compression. */
|
|
2760
3102
|
async exportGraphWithCompression(graph, format, options) {
|
|
2761
3103
|
const shouldStream = options?.streaming || options?.outputPath && graph.entities.length >= STREAMING_CONFIG.STREAMING_THRESHOLD;
|
|
@@ -2797,7 +3139,7 @@ var init_IOManager = __esm({
|
|
|
2797
3139
|
}
|
|
2798
3140
|
/** Stream export to a file for large graphs. */
|
|
2799
3141
|
async streamExport(format, graph, options) {
|
|
2800
|
-
const validatedOutputPath = validateFilePath(options.outputPath);
|
|
3142
|
+
const validatedOutputPath = validateFilePath(options.outputPath, void 0, false);
|
|
2801
3143
|
const exporter = new StreamingExporter(validatedOutputPath);
|
|
2802
3144
|
let result;
|
|
2803
3145
|
switch (format) {
|
|
@@ -3568,7 +3910,10 @@ var init_IOManager = __esm({
|
|
|
3568
3910
|
async restoreFromBackup(backupPath) {
|
|
3569
3911
|
try {
|
|
3570
3912
|
validateFilePath(backupPath, this.backupDir, true);
|
|
3571
|
-
await fs.
|
|
3913
|
+
const stat = await fs.lstat(backupPath);
|
|
3914
|
+
if (stat.isSymbolicLink()) {
|
|
3915
|
+
throw new FileOperationError("Symbolic links are not allowed for backup restore", backupPath);
|
|
3916
|
+
}
|
|
3572
3917
|
const isCompressed = hasBrotliExtension(backupPath);
|
|
3573
3918
|
const backupBuffer = await fs.readFile(backupPath);
|
|
3574
3919
|
let backupContent;
|
|
@@ -3598,7 +3943,8 @@ var init_IOManager = __esm({
|
|
|
3598
3943
|
validateFilePath(backupPath, this.backupDir, true);
|
|
3599
3944
|
await fs.unlink(backupPath);
|
|
3600
3945
|
try {
|
|
3601
|
-
const
|
|
3946
|
+
const baseName = backupPath.split(/[/\\]/).pop() ?? "";
|
|
3947
|
+
const metaPath = join(this.backupDir, `${baseName}.meta.json`);
|
|
3602
3948
|
validateFilePath(metaPath, this.backupDir, true);
|
|
3603
3949
|
await fs.unlink(metaPath);
|
|
3604
3950
|
} catch {
|
|
@@ -3649,10 +3995,10 @@ var init_IOManager = __esm({
|
|
|
3649
3995
|
skippedDuplicates: 0,
|
|
3650
3996
|
entityNames: []
|
|
3651
3997
|
};
|
|
3652
|
-
const { createHash } = await import("crypto");
|
|
3998
|
+
const { createHash: createHash2 } = await import("crypto");
|
|
3653
3999
|
const graph = await this.storage.loadGraph();
|
|
3654
4000
|
const existingObsSet = new Set(
|
|
3655
|
-
graph.entities.map((e) =>
|
|
4001
|
+
graph.entities.map((e) => createHash2("sha256").update(e.observations.join("\n")).digest("hex"))
|
|
3656
4002
|
);
|
|
3657
4003
|
const { EntityManager: EntityManager2 } = await Promise.resolve().then(() => (init_EntityManager(), EntityManager_exports));
|
|
3658
4004
|
const em = new EntityManager2(this.storage);
|
|
@@ -3663,7 +4009,7 @@ var init_IOManager = __esm({
|
|
|
3663
4009
|
const chunk = chunks[i];
|
|
3664
4010
|
const entityName = `${source}-${String(i + 1).padStart(3, "0")}`;
|
|
3665
4011
|
const observations = chunk.map((m) => `[${m.role}] ${m.content}`);
|
|
3666
|
-
const obsKey =
|
|
4012
|
+
const obsKey = createHash2("sha256").update(observations.join("\n")).digest("hex");
|
|
3667
4013
|
if (existingObsSet.has(obsKey)) {
|
|
3668
4014
|
result.skippedDuplicates++;
|
|
3669
4015
|
continue;
|
|
@@ -3741,19 +4087,24 @@ var init_IOManager = __esm({
|
|
|
3741
4087
|
/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/m
|
|
3742
4088
|
];
|
|
3743
4089
|
let rawSessions = [content];
|
|
4090
|
+
const MAX_SPLIT_LENGTH = 10 * 1024 * 1024;
|
|
4091
|
+
const splitContent = content.length > MAX_SPLIT_LENGTH ? content.slice(0, MAX_SPLIT_LENGTH) : content;
|
|
3744
4092
|
for (const delimiter of delimiters) {
|
|
3745
|
-
const
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
const
|
|
4093
|
+
const flags = delimiter.flags.includes("g") ? delimiter.flags : delimiter.flags + "g";
|
|
4094
|
+
const globalDelimiter = new RegExp(delimiter.source, flags);
|
|
4095
|
+
if (globalDelimiter.test(splitContent)) {
|
|
4096
|
+
const lookaheadFlags = delimiter.flags.replace(/[gm]/g, "") + "m";
|
|
4097
|
+
const lookaheadDelimiter = new RegExp(`(?=${delimiter.source})`, lookaheadFlags);
|
|
4098
|
+
const parts = splitContent.split(lookaheadDelimiter);
|
|
4099
|
+
const MAX_PARTS = 1e4;
|
|
3749
4100
|
if (parts.length > 1) {
|
|
3750
|
-
rawSessions = parts;
|
|
4101
|
+
rawSessions = parts.slice(0, MAX_PARTS);
|
|
3751
4102
|
break;
|
|
3752
4103
|
}
|
|
3753
|
-
const splitDelimiter = new RegExp(delimiter.source,
|
|
3754
|
-
const splitParts =
|
|
4104
|
+
const splitDelimiter = new RegExp(delimiter.source, flags);
|
|
4105
|
+
const splitParts = splitContent.split(splitDelimiter);
|
|
3755
4106
|
if (splitParts.length > 1) {
|
|
3756
|
-
rawSessions = splitParts;
|
|
4107
|
+
rawSessions = splitParts.slice(0, MAX_PARTS);
|
|
3757
4108
|
break;
|
|
3758
4109
|
}
|
|
3759
4110
|
}
|
|
@@ -4606,8 +4957,15 @@ var init_GraphEventEmitter = __esm({
|
|
|
4606
4957
|
|
|
4607
4958
|
// src/core/GraphStorage.ts
|
|
4608
4959
|
import { promises as fs2 } from "fs";
|
|
4960
|
+
import { randomBytes } from "crypto";
|
|
4609
4961
|
import { Mutex } from "async-mutex";
|
|
4610
|
-
|
|
4962
|
+
function copyOptionalPersistedFields(src, dst) {
|
|
4963
|
+
for (const field of OPTIONAL_PERSISTED_ENTITY_FIELDS) {
|
|
4964
|
+
const v = src[field];
|
|
4965
|
+
if (v !== void 0) dst[field] = v;
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
var OPTIONAL_PERSISTED_ENTITY_FIELDS, GraphStorage;
|
|
4611
4969
|
var init_GraphStorage = __esm({
|
|
4612
4970
|
"src/core/GraphStorage.ts"() {
|
|
4613
4971
|
"use strict";
|
|
@@ -4617,6 +4975,60 @@ var init_GraphStorage = __esm({
|
|
|
4617
4975
|
init_utils();
|
|
4618
4976
|
init_TransactionManager();
|
|
4619
4977
|
init_GraphEventEmitter();
|
|
4978
|
+
OPTIONAL_PERSISTED_ENTITY_FIELDS = [
|
|
4979
|
+
// Core Entity (types/types.ts)
|
|
4980
|
+
"tags",
|
|
4981
|
+
"importance",
|
|
4982
|
+
"parentId",
|
|
4983
|
+
"projectId",
|
|
4984
|
+
"version",
|
|
4985
|
+
"parentEntityName",
|
|
4986
|
+
"rootEntityName",
|
|
4987
|
+
"isLatest",
|
|
4988
|
+
"supersededBy",
|
|
4989
|
+
"contentHash",
|
|
4990
|
+
"ttl",
|
|
4991
|
+
"confidence",
|
|
4992
|
+
// η.4.4 temporal versioning expansion
|
|
4993
|
+
"validFrom",
|
|
4994
|
+
"validUntil",
|
|
4995
|
+
"observationMeta",
|
|
4996
|
+
// AgentEntity extension (types/agent-memory.ts)
|
|
4997
|
+
"memoryType",
|
|
4998
|
+
"sessionId",
|
|
4999
|
+
"conversationId",
|
|
5000
|
+
"taskId",
|
|
5001
|
+
"expiresAt",
|
|
5002
|
+
"isWorkingMemory",
|
|
5003
|
+
"promotedAt",
|
|
5004
|
+
"promotedFrom",
|
|
5005
|
+
"markedForPromotion",
|
|
5006
|
+
"accessCount",
|
|
5007
|
+
"lastAccessedAt",
|
|
5008
|
+
"accessPattern",
|
|
5009
|
+
"confirmationCount",
|
|
5010
|
+
"decayRate",
|
|
5011
|
+
"agentId",
|
|
5012
|
+
"visibility",
|
|
5013
|
+
"source",
|
|
5014
|
+
// SessionEntity extension (types/agent-memory.ts)
|
|
5015
|
+
"startedAt",
|
|
5016
|
+
"endedAt",
|
|
5017
|
+
"status",
|
|
5018
|
+
"goalDescription",
|
|
5019
|
+
"taskType",
|
|
5020
|
+
"userIntent",
|
|
5021
|
+
"memoryCount",
|
|
5022
|
+
"consolidatedCount",
|
|
5023
|
+
"previousSessionId",
|
|
5024
|
+
"relatedSessionIds",
|
|
5025
|
+
"outcome",
|
|
5026
|
+
"failureCauses",
|
|
5027
|
+
// ArtifactEntity extension (types/artifact.ts)
|
|
5028
|
+
"artifactType",
|
|
5029
|
+
"toolName",
|
|
5030
|
+
"shortId"
|
|
5031
|
+
];
|
|
4620
5032
|
GraphStorage = class {
|
|
4621
5033
|
/**
|
|
4622
5034
|
* Mutex for thread-safe access to storage operations.
|
|
@@ -4687,7 +5099,7 @@ var init_GraphStorage = __esm({
|
|
|
4687
5099
|
* @throws {FileOperationError} If path traversal is detected
|
|
4688
5100
|
*/
|
|
4689
5101
|
constructor(memoryFilePath) {
|
|
4690
|
-
this.memoryFilePath = validateFilePath(memoryFilePath);
|
|
5102
|
+
this.memoryFilePath = validateFilePath(memoryFilePath, void 0, false);
|
|
4691
5103
|
}
|
|
4692
5104
|
// ==================== Phase 10 Sprint 2: Event Emitter Access ====================
|
|
4693
5105
|
/**
|
|
@@ -4713,6 +5125,25 @@ var init_GraphStorage = __esm({
|
|
|
4713
5125
|
get events() {
|
|
4714
5126
|
return this.eventEmitter;
|
|
4715
5127
|
}
|
|
5128
|
+
/**
|
|
5129
|
+
* Synchronous access to the in-memory cached graph. Returns `null` if the
|
|
5130
|
+
* cache is not yet warm — in which case consumers should call
|
|
5131
|
+
* `loadGraph()` once to populate it and then use this accessor on
|
|
5132
|
+
* subsequent reads.
|
|
5133
|
+
*
|
|
5134
|
+
* Intended for integrations that need a synchronous read path backed by
|
|
5135
|
+
* `GraphStorage`'s already-materialized cache (e.g., the `ObservableDataModel`
|
|
5136
|
+
* adapter consumed by JSON-UI's `DataProvider`, which must supply a
|
|
5137
|
+
* synchronous `snapshot()` to React's `useSyncExternalStore`). Most
|
|
5138
|
+
* callers should prefer `loadGraph()`, which lazy-loads on first call.
|
|
5139
|
+
*
|
|
5140
|
+
* The returned reference is the live cache object — do NOT mutate it.
|
|
5141
|
+
* Use `loadGraph()` for a defensive read or `getGraphForMutation()` for
|
|
5142
|
+
* a mutable copy.
|
|
5143
|
+
*/
|
|
5144
|
+
get cachedGraph() {
|
|
5145
|
+
return this.cache;
|
|
5146
|
+
}
|
|
4716
5147
|
// ==================== Durable File Operations ====================
|
|
4717
5148
|
/**
|
|
4718
5149
|
* Write content to file with fsync for durability.
|
|
@@ -4720,7 +5151,7 @@ var init_GraphStorage = __esm({
|
|
|
4720
5151
|
* @param content - Content to write
|
|
4721
5152
|
*/
|
|
4722
5153
|
async durableWriteFile(content) {
|
|
4723
|
-
const tmpPath = `${this.memoryFilePath}.tmp.${process.pid}`;
|
|
5154
|
+
const tmpPath = `${this.memoryFilePath}.tmp.${process.pid}.${randomBytes(6).toString("hex")}`;
|
|
4724
5155
|
const fd = await fs2.open(tmpPath, "w");
|
|
4725
5156
|
try {
|
|
4726
5157
|
await fd.write(content);
|
|
@@ -4913,15 +5344,7 @@ var init_GraphStorage = __esm({
|
|
|
4913
5344
|
createdAt: entity.createdAt,
|
|
4914
5345
|
lastModified: entity.lastModified
|
|
4915
5346
|
};
|
|
4916
|
-
|
|
4917
|
-
if (entity.importance !== void 0) entityData.importance = entity.importance;
|
|
4918
|
-
if (entity.parentId !== void 0) entityData.parentId = entity.parentId;
|
|
4919
|
-
if (entity.projectId !== void 0) entityData.projectId = entity.projectId;
|
|
4920
|
-
if (entity.version !== void 0) entityData.version = entity.version;
|
|
4921
|
-
if (entity.parentEntityName !== void 0) entityData.parentEntityName = entity.parentEntityName;
|
|
4922
|
-
if (entity.rootEntityName !== void 0) entityData.rootEntityName = entity.rootEntityName;
|
|
4923
|
-
if (entity.isLatest !== void 0) entityData.isLatest = entity.isLatest;
|
|
4924
|
-
if (entity.supersededBy !== void 0) entityData.supersededBy = entity.supersededBy;
|
|
5347
|
+
copyOptionalPersistedFields(entity, entityData);
|
|
4925
5348
|
const line = JSON.stringify(entityData);
|
|
4926
5349
|
try {
|
|
4927
5350
|
const stat = await fs2.stat(this.memoryFilePath);
|
|
@@ -5035,15 +5458,7 @@ var init_GraphStorage = __esm({
|
|
|
5035
5458
|
createdAt: e.createdAt,
|
|
5036
5459
|
lastModified: e.lastModified
|
|
5037
5460
|
};
|
|
5038
|
-
|
|
5039
|
-
if (e.importance !== void 0) entityData.importance = e.importance;
|
|
5040
|
-
if (e.parentId !== void 0) entityData.parentId = e.parentId;
|
|
5041
|
-
if (e.projectId !== void 0) entityData.projectId = e.projectId;
|
|
5042
|
-
if (e.version !== void 0) entityData.version = e.version;
|
|
5043
|
-
if (e.parentEntityName !== void 0) entityData.parentEntityName = e.parentEntityName;
|
|
5044
|
-
if (e.rootEntityName !== void 0) entityData.rootEntityName = e.rootEntityName;
|
|
5045
|
-
if (e.isLatest !== void 0) entityData.isLatest = e.isLatest;
|
|
5046
|
-
if (e.supersededBy !== void 0) entityData.supersededBy = e.supersededBy;
|
|
5461
|
+
copyOptionalPersistedFields(e, entityData);
|
|
5047
5462
|
return JSON.stringify(entityData);
|
|
5048
5463
|
}),
|
|
5049
5464
|
// Serialize relations with metadata (Phase 1 Sprint 5)
|
|
@@ -5121,15 +5536,7 @@ var init_GraphStorage = __esm({
|
|
|
5121
5536
|
createdAt: updatedEntity.createdAt,
|
|
5122
5537
|
lastModified: updatedEntity.lastModified
|
|
5123
5538
|
};
|
|
5124
|
-
|
|
5125
|
-
if (updatedEntity.importance !== void 0) entityData.importance = updatedEntity.importance;
|
|
5126
|
-
if (updatedEntity.parentId !== void 0) entityData.parentId = updatedEntity.parentId;
|
|
5127
|
-
if (updatedEntity.projectId !== void 0) entityData.projectId = updatedEntity.projectId;
|
|
5128
|
-
if (updatedEntity.version !== void 0) entityData.version = updatedEntity.version;
|
|
5129
|
-
if (updatedEntity.parentEntityName !== void 0) entityData.parentEntityName = updatedEntity.parentEntityName;
|
|
5130
|
-
if (updatedEntity.rootEntityName !== void 0) entityData.rootEntityName = updatedEntity.rootEntityName;
|
|
5131
|
-
if (updatedEntity.isLatest !== void 0) entityData.isLatest = updatedEntity.isLatest;
|
|
5132
|
-
if (updatedEntity.supersededBy !== void 0) entityData.supersededBy = updatedEntity.supersededBy;
|
|
5539
|
+
copyOptionalPersistedFields(updatedEntity, entityData);
|
|
5133
5540
|
const line = JSON.stringify(entityData);
|
|
5134
5541
|
try {
|
|
5135
5542
|
const stat = await fs2.stat(this.memoryFilePath);
|
|
@@ -5370,7 +5777,7 @@ var init_SQLiteStorage = __esm({
|
|
|
5370
5777
|
init_searchCache();
|
|
5371
5778
|
init_indexes();
|
|
5372
5779
|
init_utils();
|
|
5373
|
-
SQLiteStorage = class {
|
|
5780
|
+
SQLiteStorage = class _SQLiteStorage {
|
|
5374
5781
|
/**
|
|
5375
5782
|
* Mutex for thread-safe access to storage operations.
|
|
5376
5783
|
* Prevents concurrent writes from corrupting the cache.
|
|
@@ -5424,7 +5831,7 @@ var init_SQLiteStorage = __esm({
|
|
|
5424
5831
|
* @throws {FileOperationError} If path traversal is detected
|
|
5425
5832
|
*/
|
|
5426
5833
|
constructor(dbFilePath) {
|
|
5427
|
-
this.validatedDbFilePath = validateFilePath(dbFilePath);
|
|
5834
|
+
this.validatedDbFilePath = validateFilePath(dbFilePath, void 0, false);
|
|
5428
5835
|
}
|
|
5429
5836
|
/**
|
|
5430
5837
|
* Initialize the database connection and schema.
|
|
@@ -5458,7 +5865,9 @@ var init_SQLiteStorage = __esm({
|
|
|
5458
5865
|
parentEntityName TEXT,
|
|
5459
5866
|
rootEntityName TEXT,
|
|
5460
5867
|
isLatest INTEGER DEFAULT 1,
|
|
5461
|
-
supersededBy TEXT
|
|
5868
|
+
supersededBy TEXT,
|
|
5869
|
+
contentHash TEXT,
|
|
5870
|
+
agentMetadata TEXT
|
|
5462
5871
|
)
|
|
5463
5872
|
`);
|
|
5464
5873
|
this.db.exec(`
|
|
@@ -5568,8 +5977,87 @@ var init_SQLiteStorage = __esm({
|
|
|
5568
5977
|
if (!columnNames.has("supersededBy")) {
|
|
5569
5978
|
this.db.exec("ALTER TABLE entities ADD COLUMN supersededBy TEXT");
|
|
5570
5979
|
}
|
|
5980
|
+
if (!columnNames.has("contentHash")) {
|
|
5981
|
+
this.db.exec("ALTER TABLE entities ADD COLUMN contentHash TEXT");
|
|
5982
|
+
}
|
|
5983
|
+
if (!columnNames.has("agentMetadata")) {
|
|
5984
|
+
this.db.exec("ALTER TABLE entities ADD COLUMN agentMetadata TEXT");
|
|
5985
|
+
}
|
|
5571
5986
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_projectId ON entities(projectId)`);
|
|
5572
5987
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_isLatest ON entities(isLatest)`);
|
|
5988
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_content_hash ON entities(contentHash)`);
|
|
5989
|
+
}
|
|
5990
|
+
/**
|
|
5991
|
+
* Fields that round-trip through the `agentMetadata` JSON blob column.
|
|
5992
|
+
* Native columns (importance, projectId, version, contentHash, etc.) are
|
|
5993
|
+
* stored separately for SQL-side queryability. Everything else lives in
|
|
5994
|
+
* the blob to avoid schema-migration drift as the type system evolves.
|
|
5995
|
+
* Mirrors `OPTIONAL_PERSISTED_ENTITY_FIELDS` in GraphStorage.ts minus
|
|
5996
|
+
* the fields that already have native columns.
|
|
5997
|
+
*/
|
|
5998
|
+
static EXTENSION_FIELDS = [
|
|
5999
|
+
// Core Entity (not in native columns)
|
|
6000
|
+
"ttl",
|
|
6001
|
+
"confidence",
|
|
6002
|
+
// η.4.4 temporal versioning expansion
|
|
6003
|
+
"validFrom",
|
|
6004
|
+
"validUntil",
|
|
6005
|
+
"observationMeta",
|
|
6006
|
+
// AgentEntity (types/agent-memory.ts)
|
|
6007
|
+
"memoryType",
|
|
6008
|
+
"sessionId",
|
|
6009
|
+
"conversationId",
|
|
6010
|
+
"taskId",
|
|
6011
|
+
"expiresAt",
|
|
6012
|
+
"isWorkingMemory",
|
|
6013
|
+
"promotedAt",
|
|
6014
|
+
"promotedFrom",
|
|
6015
|
+
"markedForPromotion",
|
|
6016
|
+
"accessCount",
|
|
6017
|
+
"lastAccessedAt",
|
|
6018
|
+
"accessPattern",
|
|
6019
|
+
"confirmationCount",
|
|
6020
|
+
"decayRate",
|
|
6021
|
+
"agentId",
|
|
6022
|
+
"visibility",
|
|
6023
|
+
"source",
|
|
6024
|
+
// SessionEntity (types/agent-memory.ts)
|
|
6025
|
+
"startedAt",
|
|
6026
|
+
"endedAt",
|
|
6027
|
+
"status",
|
|
6028
|
+
"goalDescription",
|
|
6029
|
+
"taskType",
|
|
6030
|
+
"userIntent",
|
|
6031
|
+
"memoryCount",
|
|
6032
|
+
"consolidatedCount",
|
|
6033
|
+
"previousSessionId",
|
|
6034
|
+
"relatedSessionIds",
|
|
6035
|
+
"outcome",
|
|
6036
|
+
"failureCauses",
|
|
6037
|
+
// ArtifactEntity (types/artifact.ts)
|
|
6038
|
+
"artifactType",
|
|
6039
|
+
"toolName",
|
|
6040
|
+
"shortId"
|
|
6041
|
+
];
|
|
6042
|
+
/** Build the JSON blob payload for the `agentMetadata` column. */
|
|
6043
|
+
serializeExtensionFields(entity) {
|
|
6044
|
+
const src = entity;
|
|
6045
|
+
const out = {};
|
|
6046
|
+
for (const field of _SQLiteStorage.EXTENSION_FIELDS) {
|
|
6047
|
+
const v = src[field];
|
|
6048
|
+
if (v !== void 0) out[field] = v;
|
|
6049
|
+
}
|
|
6050
|
+
return Object.keys(out).length === 0 ? null : JSON.stringify(out);
|
|
6051
|
+
}
|
|
6052
|
+
/** Inverse of `serializeExtensionFields`. Tolerant of malformed JSON. */
|
|
6053
|
+
parseExtensionFields(blob) {
|
|
6054
|
+
if (!blob) return {};
|
|
6055
|
+
try {
|
|
6056
|
+
const parsed = JSON.parse(blob);
|
|
6057
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
6058
|
+
} catch {
|
|
6059
|
+
return {};
|
|
6060
|
+
}
|
|
5573
6061
|
}
|
|
5574
6062
|
/**
|
|
5575
6063
|
* Load all data from SQLite into memory cache.
|
|
@@ -5596,11 +6084,23 @@ var init_SQLiteStorage = __esm({
|
|
|
5596
6084
|
* Convert a database row to an Entity object.
|
|
5597
6085
|
*/
|
|
5598
6086
|
rowToEntity(row) {
|
|
6087
|
+
let observations;
|
|
6088
|
+
let tags;
|
|
6089
|
+
try {
|
|
6090
|
+
observations = JSON.parse(row.observations);
|
|
6091
|
+
} catch {
|
|
6092
|
+
observations = [];
|
|
6093
|
+
}
|
|
6094
|
+
try {
|
|
6095
|
+
tags = row.tags ? JSON.parse(row.tags) : void 0;
|
|
6096
|
+
} catch {
|
|
6097
|
+
tags = void 0;
|
|
6098
|
+
}
|
|
5599
6099
|
const entity = {
|
|
5600
6100
|
name: row.name,
|
|
5601
6101
|
entityType: row.entityType,
|
|
5602
|
-
observations
|
|
5603
|
-
tags
|
|
6102
|
+
observations,
|
|
6103
|
+
tags,
|
|
5604
6104
|
importance: row.importance ?? void 0,
|
|
5605
6105
|
parentId: row.parentId ?? void 0,
|
|
5606
6106
|
createdAt: row.createdAt,
|
|
@@ -5612,6 +6112,9 @@ var init_SQLiteStorage = __esm({
|
|
|
5612
6112
|
if (row.rootEntityName != null) entity.rootEntityName = row.rootEntityName;
|
|
5613
6113
|
if (row.isLatest != null) entity.isLatest = row.isLatest === 1;
|
|
5614
6114
|
if (row.supersededBy != null) entity.supersededBy = row.supersededBy;
|
|
6115
|
+
if (row.contentHash != null) entity.contentHash = row.contentHash;
|
|
6116
|
+
const ext = this.parseExtensionFields(row.agentMetadata ?? null);
|
|
6117
|
+
Object.assign(entity, ext);
|
|
5615
6118
|
return entity;
|
|
5616
6119
|
}
|
|
5617
6120
|
/**
|
|
@@ -5628,8 +6131,18 @@ var init_SQLiteStorage = __esm({
|
|
|
5628
6131
|
};
|
|
5629
6132
|
if (row.weight !== null) relation.weight = row.weight;
|
|
5630
6133
|
if (row.confidence !== null) relation.confidence = row.confidence;
|
|
5631
|
-
if (row.properties !== null)
|
|
5632
|
-
|
|
6134
|
+
if (row.properties !== null) {
|
|
6135
|
+
try {
|
|
6136
|
+
relation.properties = JSON.parse(row.properties);
|
|
6137
|
+
} catch {
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
if (row.metadata !== null) {
|
|
6141
|
+
try {
|
|
6142
|
+
relation.metadata = JSON.parse(row.metadata);
|
|
6143
|
+
} catch {
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
5633
6146
|
return relation;
|
|
5634
6147
|
}
|
|
5635
6148
|
/**
|
|
@@ -5710,8 +6223,8 @@ var init_SQLiteStorage = __esm({
|
|
|
5710
6223
|
this.db.exec("DELETE FROM relations");
|
|
5711
6224
|
this.db.exec("DELETE FROM entities");
|
|
5712
6225
|
const entityStmt = this.db.prepare(`
|
|
5713
|
-
INSERT INTO entities (name, entityType, observations, tags, importance, parentId, createdAt, lastModified, projectId, version, parentEntityName, rootEntityName, isLatest, supersededBy)
|
|
5714
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6226
|
+
INSERT INTO entities (name, entityType, observations, tags, importance, parentId, createdAt, lastModified, projectId, version, parentEntityName, rootEntityName, isLatest, supersededBy, contentHash, agentMetadata)
|
|
6227
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5715
6228
|
`);
|
|
5716
6229
|
for (const entity of graph.entities) {
|
|
5717
6230
|
entityStmt.run(
|
|
@@ -5728,7 +6241,9 @@ var init_SQLiteStorage = __esm({
|
|
|
5728
6241
|
entity.parentEntityName ?? null,
|
|
5729
6242
|
entity.rootEntityName ?? null,
|
|
5730
6243
|
entity.isLatest === false ? 0 : 1,
|
|
5731
|
-
entity.supersededBy ?? null
|
|
6244
|
+
entity.supersededBy ?? null,
|
|
6245
|
+
entity.contentHash ?? null,
|
|
6246
|
+
this.serializeExtensionFields(entity)
|
|
5732
6247
|
);
|
|
5733
6248
|
}
|
|
5734
6249
|
const relationStmt = this.db.prepare(`
|
|
@@ -5776,8 +6291,8 @@ var init_SQLiteStorage = __esm({
|
|
|
5776
6291
|
return this.mutex.runExclusive(async () => {
|
|
5777
6292
|
if (!this.db) throw new Error("Database not initialized");
|
|
5778
6293
|
const stmt = this.db.prepare(`
|
|
5779
|
-
INSERT OR REPLACE INTO entities (name, entityType, observations, tags, importance, parentId, createdAt, lastModified, projectId, version, parentEntityName, rootEntityName, isLatest, supersededBy)
|
|
5780
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6294
|
+
INSERT OR REPLACE INTO entities (name, entityType, observations, tags, importance, parentId, createdAt, lastModified, projectId, version, parentEntityName, rootEntityName, isLatest, supersededBy, contentHash, agentMetadata)
|
|
6295
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5781
6296
|
`);
|
|
5782
6297
|
stmt.run(
|
|
5783
6298
|
entity.name,
|
|
@@ -5793,7 +6308,9 @@ var init_SQLiteStorage = __esm({
|
|
|
5793
6308
|
entity.parentEntityName ?? null,
|
|
5794
6309
|
entity.rootEntityName ?? null,
|
|
5795
6310
|
entity.isLatest === false ? 0 : 1,
|
|
5796
|
-
entity.supersededBy ?? null
|
|
6311
|
+
entity.supersededBy ?? null,
|
|
6312
|
+
entity.contentHash ?? null,
|
|
6313
|
+
this.serializeExtensionFields(entity)
|
|
5797
6314
|
);
|
|
5798
6315
|
const existingIndex = this.cache.entities.findIndex((e) => e.name === entity.name);
|
|
5799
6316
|
if (existingIndex >= 0) {
|
|
@@ -5882,7 +6399,9 @@ var init_SQLiteStorage = __esm({
|
|
|
5882
6399
|
parentEntityName = ?,
|
|
5883
6400
|
rootEntityName = ?,
|
|
5884
6401
|
isLatest = ?,
|
|
5885
|
-
supersededBy =
|
|
6402
|
+
supersededBy = ?,
|
|
6403
|
+
contentHash = ?,
|
|
6404
|
+
agentMetadata = ?
|
|
5886
6405
|
WHERE name = ?
|
|
5887
6406
|
`);
|
|
5888
6407
|
stmt.run(
|
|
@@ -5898,6 +6417,8 @@ var init_SQLiteStorage = __esm({
|
|
|
5898
6417
|
entity.rootEntityName ?? null,
|
|
5899
6418
|
entity.isLatest === false ? 0 : 1,
|
|
5900
6419
|
entity.supersededBy ?? null,
|
|
6420
|
+
entity.contentHash ?? null,
|
|
6421
|
+
this.serializeExtensionFields(entity),
|
|
5901
6422
|
entityName
|
|
5902
6423
|
);
|
|
5903
6424
|
this.nameIndex.add(entity);
|
|
@@ -6697,6 +7218,12 @@ var init_ObservationManager = __esm({
|
|
|
6697
7218
|
contradictionDetector;
|
|
6698
7219
|
linkedEntityManager;
|
|
6699
7220
|
_autoLinker;
|
|
7221
|
+
/** Lazy provider for the validator — invoking it constructs / fetches
|
|
7222
|
+
* the MemoryValidator. Stored as a thunk so unconditional wiring at
|
|
7223
|
+
* `ManagerContext` construction time costs nothing until the validator
|
|
7224
|
+
* is actually needed (i.e., MEMORY_VALIDATE_ON_STORE flips on AND an
|
|
7225
|
+
* observation gets added). */
|
|
7226
|
+
memoryValidatorProvider;
|
|
6700
7227
|
/**
|
|
6701
7228
|
* Enable contradiction detection on addObservations.
|
|
6702
7229
|
* When a new observation is detected as contradicting an existing one,
|
|
@@ -6712,6 +7239,37 @@ var init_ObservationManager = __esm({
|
|
|
6712
7239
|
setAutoLinker(autoLinker) {
|
|
6713
7240
|
this._autoLinker = autoLinker;
|
|
6714
7241
|
}
|
|
7242
|
+
/**
|
|
7243
|
+
* Wire a `MemoryValidator` provider for the optional pre-storage
|
|
7244
|
+
* validation hook (Phase δ.1, T31). The argument is a thunk so the
|
|
7245
|
+
* validator can be lazy-constructed only when actually needed —
|
|
7246
|
+
* `ManagerContext` wires this unconditionally at construction time so
|
|
7247
|
+
* runtime toggling of `MEMORY_VALIDATE_ON_STORE` works in both
|
|
7248
|
+
* directions, but the validator object itself isn't built until the
|
|
7249
|
+
* first observation is added with the flag on.
|
|
7250
|
+
*
|
|
7251
|
+
* Behaviour when flag is on:
|
|
7252
|
+
* - `duplicate-observation` → blocking; observation skipped with a
|
|
7253
|
+
* `console.warn`.
|
|
7254
|
+
* - `semantic-contradiction` → ADVISORY; if a `ContradictionDetector`
|
|
7255
|
+
* is also wired (the v1.8.0 supersede branch), that branch handles
|
|
7256
|
+
* the case downstream and creates a proper version chain. Filtering
|
|
7257
|
+
* it here would silently disable supersede semantics.
|
|
7258
|
+
* - `low-confidence` → ADVISORY only.
|
|
7259
|
+
*
|
|
7260
|
+
* Default off — preserves backwards-compat for existing callers.
|
|
7261
|
+
*
|
|
7262
|
+
* Overload: accepts either a validator instance (eager) or a thunk
|
|
7263
|
+
* (lazy). Pass the instance for tests where a stub is convenient;
|
|
7264
|
+
* pass the thunk for production wiring through `ManagerContext`.
|
|
7265
|
+
*/
|
|
7266
|
+
setMemoryValidator(validatorOrProvider) {
|
|
7267
|
+
if (typeof validatorOrProvider === "function") {
|
|
7268
|
+
this.memoryValidatorProvider = validatorOrProvider;
|
|
7269
|
+
} else {
|
|
7270
|
+
this.memoryValidatorProvider = () => validatorOrProvider;
|
|
7271
|
+
}
|
|
7272
|
+
}
|
|
6715
7273
|
/**
|
|
6716
7274
|
* Resolve deduplication options from explicit parameter and environment variable.
|
|
6717
7275
|
*
|
|
@@ -6780,7 +7338,31 @@ var init_ObservationManager = __esm({
|
|
|
6780
7338
|
if (!entity) {
|
|
6781
7339
|
throw new EntityNotFoundError(o.entityName);
|
|
6782
7340
|
}
|
|
6783
|
-
|
|
7341
|
+
let nonExactDuplicates = o.contents.filter((content) => !entity.observations.includes(content));
|
|
7342
|
+
if (this.memoryValidatorProvider && process.env.MEMORY_VALIDATE_ON_STORE === "true") {
|
|
7343
|
+
const validator = this.memoryValidatorProvider();
|
|
7344
|
+
const passed = [];
|
|
7345
|
+
for (const content of nonExactDuplicates) {
|
|
7346
|
+
const result = await validator.validateConsistency(content, entity);
|
|
7347
|
+
const blockingDup = result.issues.some((i) => i.kind === "duplicate-observation");
|
|
7348
|
+
if (!blockingDup) {
|
|
7349
|
+
passed.push(content);
|
|
7350
|
+
if (!result.isValid) {
|
|
7351
|
+
const advisories = result.issues.filter((i) => i.kind !== "duplicate-observation").map((i) => i.kind);
|
|
7352
|
+
if (advisories.length > 0) {
|
|
7353
|
+
console.warn(
|
|
7354
|
+
`[ObservationManager] Validator advisory for "${o.entityName}": ${advisories.join(", ")}. ` + (this.contradictionDetector ? "Semantic-contradiction findings will be handled by the v1.8.0 supersede branch." : "No contradiction-detector wired; advisory only.")
|
|
7355
|
+
);
|
|
7356
|
+
}
|
|
7357
|
+
}
|
|
7358
|
+
} else {
|
|
7359
|
+
console.warn(
|
|
7360
|
+
`[ObservationManager] Skipping duplicate observation on entity "${o.entityName}". Suggestions: ${result.suggestions.join("; ")}`
|
|
7361
|
+
);
|
|
7362
|
+
}
|
|
7363
|
+
}
|
|
7364
|
+
nonExactDuplicates = passed;
|
|
7365
|
+
}
|
|
6784
7366
|
if (nonExactDuplicates.length > 0) {
|
|
6785
7367
|
if (this.contradictionDetector && this.linkedEntityManager) {
|
|
6786
7368
|
const contradictions = await this.contradictionDetector.detect(
|
|
@@ -6936,6 +7518,74 @@ var init_ObservationManager = __esm({
|
|
|
6936
7518
|
await this.storage.saveGraph(graph);
|
|
6937
7519
|
}
|
|
6938
7520
|
}
|
|
7521
|
+
// ==================== η.4.4: Temporal Validity ====================
|
|
7522
|
+
//
|
|
7523
|
+
// Per-observation temporal validity via the parallel `observationMeta[]`
|
|
7524
|
+
// array on Entity. Mirrors the entity-level `validFrom`/`validUntil` shape
|
|
7525
|
+
// but indexed by observation content (not array position) so re-ordering
|
|
7526
|
+
// observations doesn't disturb validity windows.
|
|
7527
|
+
/**
|
|
7528
|
+
* Mark a specific observation as no longer valid by setting its
|
|
7529
|
+
* `validUntil`. Creates the parallel `observationMeta[]` entry if absent
|
|
7530
|
+
* (preserves backwards-compat for entities that don't use the bitemporal
|
|
7531
|
+
* axis). Idempotent: a second call updates the existing `validUntil`.
|
|
7532
|
+
*
|
|
7533
|
+
* @throws {EntityNotFoundError} If no entity exists with the given name
|
|
7534
|
+
* @throws {ValidationError} If the observation isn't found on the entity
|
|
7535
|
+
*/
|
|
7536
|
+
async invalidateObservation(entityName, content, ended) {
|
|
7537
|
+
const release = await this.storage.graphMutex.acquire();
|
|
7538
|
+
try {
|
|
7539
|
+
const graph = await this.storage.getGraphForMutation();
|
|
7540
|
+
const entity = graph.entities.find((e) => e.name === entityName);
|
|
7541
|
+
if (!entity) throw new EntityNotFoundError(entityName);
|
|
7542
|
+
if (!entity.observations.includes(content)) {
|
|
7543
|
+
throw new ValidationError(
|
|
7544
|
+
`Observation not found on entity '${entityName}'`,
|
|
7545
|
+
[`content: ${JSON.stringify(content).slice(0, 80)}`]
|
|
7546
|
+
);
|
|
7547
|
+
}
|
|
7548
|
+
const ts = ended ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
7549
|
+
if (!entity.observationMeta) entity.observationMeta = [];
|
|
7550
|
+
const existing = entity.observationMeta.find((m) => m.content === content);
|
|
7551
|
+
if (existing) {
|
|
7552
|
+
existing.validUntil = ts;
|
|
7553
|
+
} else {
|
|
7554
|
+
entity.observationMeta.push({ content, validUntil: ts });
|
|
7555
|
+
}
|
|
7556
|
+
entity.lastModified = (/* @__PURE__ */ new Date()).toISOString();
|
|
7557
|
+
await this.storage.saveGraph(graph);
|
|
7558
|
+
} finally {
|
|
7559
|
+
release();
|
|
7560
|
+
}
|
|
7561
|
+
}
|
|
7562
|
+
/**
|
|
7563
|
+
* Return observations valid at a given point in time. An observation
|
|
7564
|
+
* with no meta entry is treated as unbounded (always valid). With a meta
|
|
7565
|
+
* entry, validity rules mirror `EntityManager.entityAsOf`:
|
|
7566
|
+
* - `validFrom` undefined OR `validFrom` <= asOf
|
|
7567
|
+
* - `validUntil` undefined OR `validUntil` >= asOf
|
|
7568
|
+
*
|
|
7569
|
+
* @throws {ValidationError} If `asOf` is not an ISO 8601 date string
|
|
7570
|
+
*/
|
|
7571
|
+
async observationsAsOf(entityName, asOf) {
|
|
7572
|
+
if (!/^\d{4}-\d{2}-\d{2}/.test(asOf)) {
|
|
7573
|
+
throw new ValidationError(`asOf must be an ISO 8601 date string, got: '${asOf}'`, []);
|
|
7574
|
+
}
|
|
7575
|
+
const graph = await this.storage.loadGraph();
|
|
7576
|
+
const entity = graph.entities.find((e) => e.name === entityName);
|
|
7577
|
+
if (!entity) return [];
|
|
7578
|
+
const metaByContent = new Map(
|
|
7579
|
+
(entity.observationMeta ?? []).map((m) => [m.content, m])
|
|
7580
|
+
);
|
|
7581
|
+
return entity.observations.filter((obs) => {
|
|
7582
|
+
const meta = metaByContent.get(obs);
|
|
7583
|
+
if (!meta) return true;
|
|
7584
|
+
if (meta.validFrom && meta.validFrom > asOf) return false;
|
|
7585
|
+
if (meta.validUntil && meta.validUntil < asOf) return false;
|
|
7586
|
+
return true;
|
|
7587
|
+
});
|
|
7588
|
+
}
|
|
6939
7589
|
};
|
|
6940
7590
|
}
|
|
6941
7591
|
});
|
|
@@ -9619,7 +10269,14 @@ var init_SavedSearchManager = __esm({
|
|
|
9619
10269
|
try {
|
|
9620
10270
|
const data = await fs4.readFile(this.savedSearchesFilePath, "utf-8");
|
|
9621
10271
|
const lines = data.split("\n").filter((line) => line.trim() !== "");
|
|
9622
|
-
|
|
10272
|
+
const searches = [];
|
|
10273
|
+
for (const line of lines) {
|
|
10274
|
+
try {
|
|
10275
|
+
searches.push(JSON.parse(line));
|
|
10276
|
+
} catch {
|
|
10277
|
+
}
|
|
10278
|
+
}
|
|
10279
|
+
return searches;
|
|
9623
10280
|
} catch (error) {
|
|
9624
10281
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
9625
10282
|
return [];
|
|
@@ -12013,7 +12670,14 @@ var init_TagManager = __esm({
|
|
|
12013
12670
|
try {
|
|
12014
12671
|
const data = await fs5.readFile(this.tagAliasesFilePath, "utf-8");
|
|
12015
12672
|
const lines = data.split("\n").filter((line) => line.trim() !== "");
|
|
12016
|
-
|
|
12673
|
+
const aliases = [];
|
|
12674
|
+
for (const line of lines) {
|
|
12675
|
+
try {
|
|
12676
|
+
aliases.push(JSON.parse(line));
|
|
12677
|
+
} catch {
|
|
12678
|
+
}
|
|
12679
|
+
}
|
|
12680
|
+
return aliases;
|
|
12017
12681
|
} catch (error) {
|
|
12018
12682
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
12019
12683
|
return [];
|
|
@@ -13247,6 +13911,7 @@ var init_AutoLinker = __esm({
|
|
|
13247
13911
|
});
|
|
13248
13912
|
candidates.sort((a, b) => b.name.length - a.name.length);
|
|
13249
13913
|
for (const entity of candidates) {
|
|
13914
|
+
if (entity.name.length > 500) continue;
|
|
13250
13915
|
const escapedName = escapeRegExp(entity.name);
|
|
13251
13916
|
const flags = opts.caseSensitive ? "" : "i";
|
|
13252
13917
|
const pattern2 = new RegExp(`\\b${escapedName}\\b`, flags);
|
|
@@ -13557,6 +14222,7 @@ var init_FactExtractor = __esm({
|
|
|
13557
14222
|
|
|
13558
14223
|
// src/core/TransitionLedger.ts
|
|
13559
14224
|
import { promises as fs7 } from "fs";
|
|
14225
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
13560
14226
|
import * as path4 from "path";
|
|
13561
14227
|
var TransitionLedger;
|
|
13562
14228
|
var init_TransitionLedger = __esm({
|
|
@@ -13591,7 +14257,7 @@ var init_TransitionLedger = __esm({
|
|
|
13591
14257
|
async append(event) {
|
|
13592
14258
|
const fullEvent = {
|
|
13593
14259
|
...event,
|
|
13594
|
-
id: `txn_${Date.now()}_${
|
|
14260
|
+
id: `txn_${Date.now()}_${randomBytes2(4).toString("hex")}`,
|
|
13595
14261
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
13596
14262
|
};
|
|
13597
14263
|
this.events.push(fullEvent);
|
|
@@ -13757,12 +14423,7 @@ var init_TransitionLedger = __esm({
|
|
|
13757
14423
|
entityId: event.entity.name,
|
|
13758
14424
|
field: "entity",
|
|
13759
14425
|
from: null,
|
|
13760
|
-
to:
|
|
13761
|
-
entityType: event.entity.entityType,
|
|
13762
|
-
observations: event.entity.observations,
|
|
13763
|
-
tags: event.entity.tags,
|
|
13764
|
-
importance: event.entity.importance
|
|
13765
|
-
}
|
|
14426
|
+
to: this.mapEntityState(event.entity)
|
|
13766
14427
|
});
|
|
13767
14428
|
})
|
|
13768
14429
|
);
|
|
@@ -13786,12 +14447,7 @@ var init_TransitionLedger = __esm({
|
|
|
13786
14447
|
handleAppend({
|
|
13787
14448
|
entityId: event.entityName,
|
|
13788
14449
|
field: "entity",
|
|
13789
|
-
from: event.entity ?
|
|
13790
|
-
entityType: event.entity.entityType,
|
|
13791
|
-
observations: event.entity.observations,
|
|
13792
|
-
tags: event.entity.tags,
|
|
13793
|
-
importance: event.entity.importance
|
|
13794
|
-
} : null,
|
|
14450
|
+
from: event.entity ? this.mapEntityState(event.entity) : null,
|
|
13795
14451
|
to: null
|
|
13796
14452
|
});
|
|
13797
14453
|
})
|
|
@@ -13849,6 +14505,18 @@ var init_TransitionLedger = __esm({
|
|
|
13849
14505
|
};
|
|
13850
14506
|
}
|
|
13851
14507
|
// ==================== Private Helpers ====================
|
|
14508
|
+
/**
|
|
14509
|
+
* Map entity state to a consistent audit format.
|
|
14510
|
+
* Extracts only the core data fields to avoid metadata noise.
|
|
14511
|
+
*/
|
|
14512
|
+
mapEntityState(entity) {
|
|
14513
|
+
return {
|
|
14514
|
+
entityType: entity.entityType,
|
|
14515
|
+
observations: entity.observations,
|
|
14516
|
+
tags: entity.tags,
|
|
14517
|
+
importance: entity.importance
|
|
14518
|
+
};
|
|
14519
|
+
}
|
|
13852
14520
|
/**
|
|
13853
14521
|
* Deep equality check for transition values.
|
|
13854
14522
|
* Handles primitives, arrays, and plain objects.
|
|
@@ -14354,6 +15022,9 @@ var init_FreshnessManager = __esm({
|
|
|
14354
15022
|
});
|
|
14355
15023
|
|
|
14356
15024
|
// src/agent/DecayEngine.ts
|
|
15025
|
+
function tokenize3(text) {
|
|
15026
|
+
return new Set(tokenize2(text));
|
|
15027
|
+
}
|
|
14357
15028
|
var DecayEngine;
|
|
14358
15029
|
var init_DecayEngine = __esm({
|
|
14359
15030
|
"src/agent/DecayEngine.ts"() {
|
|
@@ -14361,6 +15032,7 @@ var init_DecayEngine = __esm({
|
|
|
14361
15032
|
init_esm_shims();
|
|
14362
15033
|
init_agent_memory();
|
|
14363
15034
|
init_FreshnessManager();
|
|
15035
|
+
init_textSimilarity();
|
|
14364
15036
|
DecayEngine = class {
|
|
14365
15037
|
storage;
|
|
14366
15038
|
accessTracker;
|
|
@@ -14369,15 +15041,20 @@ var init_DecayEngine = __esm({
|
|
|
14369
15041
|
constructor(storage, accessTracker, config = {}) {
|
|
14370
15042
|
this.storage = storage;
|
|
14371
15043
|
this.accessTracker = accessTracker;
|
|
15044
|
+
const halfLifeHours = config.halfLifeHours ?? 168;
|
|
14372
15045
|
this.config = {
|
|
14373
|
-
halfLifeHours
|
|
14374
|
-
// 1 week
|
|
15046
|
+
halfLifeHours,
|
|
14375
15047
|
importanceModulation: config.importanceModulation ?? true,
|
|
14376
15048
|
accessModulation: config.accessModulation ?? true,
|
|
14377
15049
|
minImportance: config.minImportance ?? 0.1,
|
|
14378
15050
|
ttlExpiredDecayMultiplier: config.ttlExpiredDecayMultiplier ?? 3,
|
|
14379
15051
|
confidenceDecayRate: config.confidenceDecayRate ?? 1e-3,
|
|
14380
|
-
applyConfidenceToImportance: config.applyConfidenceToImportance ?? false
|
|
15052
|
+
applyConfidenceToImportance: config.applyConfidenceToImportance ?? false,
|
|
15053
|
+
// PRD MEM-01: derive decayRate from halfLifeHours when not given.
|
|
15054
|
+
decayRate: config.decayRate ?? Math.LN2 / (halfLifeHours * 3600),
|
|
15055
|
+
freshnessCoefficient: config.freshnessCoefficient ?? 0.01,
|
|
15056
|
+
relevanceWeight: config.relevanceWeight ?? 0.35,
|
|
15057
|
+
minImportanceThreshold: config.minImportanceThreshold ?? 0.1
|
|
14381
15058
|
};
|
|
14382
15059
|
this.freshnessManager = new FreshnessManager(storage, {
|
|
14383
15060
|
defaultHalfLifeHours: this.config.halfLifeHours
|
|
@@ -14518,6 +15195,58 @@ var init_DecayEngine = __esm({
|
|
|
14518
15195
|
getFreshnessManager() {
|
|
14519
15196
|
return this.freshnessManager;
|
|
14520
15197
|
}
|
|
15198
|
+
// ==================== PRD-aligned Effective Importance (v1.12.0) ====================
|
|
15199
|
+
/**
|
|
15200
|
+
* Calculate effective importance using the Context Engine PRD formula.
|
|
15201
|
+
*
|
|
15202
|
+
* Distinct from `calculateEffectiveImportance` (legacy formula preserved
|
|
15203
|
+
* for `DecayScheduler`, `SearchManager`, `SemanticForget`, etc.).
|
|
15204
|
+
*
|
|
15205
|
+
* Formula:
|
|
15206
|
+
* ```
|
|
15207
|
+
* effective = importance × recency × freshness + relevance_boost
|
|
15208
|
+
* recency = e^(−decayRate × age_seconds)
|
|
15209
|
+
* freshness = e^(−freshnessCoefficient × seconds_since_last_access)
|
|
15210
|
+
* relevance_boost = (|query_tokens ∩ turn_tokens| / |query_tokens|) × relevanceWeight
|
|
15211
|
+
* ```
|
|
15212
|
+
*
|
|
15213
|
+
* `importance` is auto-scaled from memoryjs's `[0, 10]` range to PRD's
|
|
15214
|
+
* `[1.0, 3.0]` range via `prd_importance = 1.0 + (memoryjs_importance / 10.0) * 2.0`.
|
|
15215
|
+
*
|
|
15216
|
+
* @param entity AgentEntity to score.
|
|
15217
|
+
* @param queryContext Optional query string. When provided, enables the
|
|
15218
|
+
* relevance-boost term via token overlap.
|
|
15219
|
+
* @param now Optional clock override (test injection). Defaults to `Date.now()`.
|
|
15220
|
+
* @returns Float in `[0, ∞)`. Callers filter via `minImportanceThreshold`.
|
|
15221
|
+
*/
|
|
15222
|
+
calculatePrdEffectiveImportance(entity, queryContext, now = Date.now()) {
|
|
15223
|
+
const memoryjsScale = entity.importance ?? 5;
|
|
15224
|
+
const prdImportance = 1 + memoryjsScale / 10 * 2;
|
|
15225
|
+
const createdAtMs = entity.createdAt ? new Date(entity.createdAt).getTime() : now;
|
|
15226
|
+
const ageSeconds = Math.max(0, (now - createdAtMs) / 1e3);
|
|
15227
|
+
const recency = Math.exp(-this.config.decayRate * ageSeconds);
|
|
15228
|
+
const lastAccessMs = entity.lastAccessedAt ? new Date(entity.lastAccessedAt).getTime() : createdAtMs;
|
|
15229
|
+
const sinceAccessSeconds = Math.max(0, (now - lastAccessMs) / 1e3);
|
|
15230
|
+
const freshness = Math.exp(-this.config.freshnessCoefficient * sinceAccessSeconds);
|
|
15231
|
+
let relevanceBoost = 0;
|
|
15232
|
+
if (queryContext && queryContext.length > 0) {
|
|
15233
|
+
const queryTokens = tokenize3(queryContext);
|
|
15234
|
+
if (queryTokens.size > 0) {
|
|
15235
|
+
const turnText = (entity.observations ?? []).join(" ");
|
|
15236
|
+
const turnTokens = tokenize3(turnText);
|
|
15237
|
+
let intersection = 0;
|
|
15238
|
+
for (const t of queryTokens) {
|
|
15239
|
+
if (turnTokens.has(t)) intersection += 1;
|
|
15240
|
+
}
|
|
15241
|
+
relevanceBoost = intersection / queryTokens.size * this.config.relevanceWeight;
|
|
15242
|
+
}
|
|
15243
|
+
}
|
|
15244
|
+
return prdImportance * recency * freshness + relevanceBoost;
|
|
15245
|
+
}
|
|
15246
|
+
/** Read-only accessor for the configured PRD `min_importance_threshold`. */
|
|
15247
|
+
get prdMinImportanceThreshold() {
|
|
15248
|
+
return this.config.minImportanceThreshold;
|
|
15249
|
+
}
|
|
14521
15250
|
// ==================== Decayed Memory Queries ====================
|
|
14522
15251
|
/**
|
|
14523
15252
|
* Get memories that have decayed below threshold.
|
|
@@ -15136,16 +15865,14 @@ var init_SummarizationService = __esm({
|
|
|
15136
15865
|
* @returns Array of summaries (one per group)
|
|
15137
15866
|
*/
|
|
15138
15867
|
async summarizeGroups(groups) {
|
|
15139
|
-
|
|
15140
|
-
|
|
15141
|
-
|
|
15142
|
-
|
|
15143
|
-
|
|
15144
|
-
|
|
15145
|
-
|
|
15146
|
-
|
|
15147
|
-
}
|
|
15148
|
-
return summaries;
|
|
15868
|
+
return Promise.all(
|
|
15869
|
+
groups.map((group) => {
|
|
15870
|
+
if (group.length === 1) {
|
|
15871
|
+
return group[0];
|
|
15872
|
+
}
|
|
15873
|
+
return this.summarize(group);
|
|
15874
|
+
})
|
|
15875
|
+
);
|
|
15149
15876
|
}
|
|
15150
15877
|
// ==================== Configuration Access ====================
|
|
15151
15878
|
/**
|
|
@@ -15984,7 +16711,7 @@ var init_ContextWindowManager = __esm({
|
|
|
15984
16711
|
init_esm_shims();
|
|
15985
16712
|
init_agent_memory();
|
|
15986
16713
|
init_ContextProfileManager();
|
|
15987
|
-
ContextWindowManager = class {
|
|
16714
|
+
ContextWindowManager = class _ContextWindowManager {
|
|
15988
16715
|
storage;
|
|
15989
16716
|
salienceEngine;
|
|
15990
16717
|
config;
|
|
@@ -16751,39 +17478,279 @@ var init_ContextWindowManager = __esm({
|
|
|
16751
17478
|
console.error("[ContextWindowManager.wakeUp] L1 entity loading failed:", err);
|
|
16752
17479
|
}
|
|
16753
17480
|
}
|
|
17481
|
+
if (options.compress && l1) {
|
|
17482
|
+
try {
|
|
17483
|
+
const level = typeof options.compress === "string" ? options.compress : "medium";
|
|
17484
|
+
const compressResult = this.compressForContext(l1, { level });
|
|
17485
|
+
if (compressResult.stats.savedTokens > 0) {
|
|
17486
|
+
l1 = compressResult.compressed;
|
|
17487
|
+
}
|
|
17488
|
+
} catch (err) {
|
|
17489
|
+
console.error("[ContextWindowManager.wakeUp] Compression failed, using uncompressed:", err);
|
|
17490
|
+
}
|
|
17491
|
+
}
|
|
16754
17492
|
const totalTokens = this.estimateStringTokens(l0) + this.estimateStringTokens(l1);
|
|
16755
17493
|
return { l0, l1, totalTokens, entityCount };
|
|
16756
17494
|
}
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
|
|
16760
|
-
|
|
16761
|
-
|
|
16762
|
-
|
|
16763
|
-
|
|
16764
|
-
|
|
16765
|
-
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
|
|
16771
|
-
|
|
16772
|
-
|
|
16773
|
-
|
|
16774
|
-
|
|
16775
|
-
|
|
16776
|
-
|
|
16777
|
-
|
|
17495
|
+
// ==================== Context Compression ====================
|
|
17496
|
+
/** Unicode abbreviation map for aggressive-level compression (code keywords). */
|
|
17497
|
+
static COMMON_PATTERNS = {
|
|
17498
|
+
"function ": "\u0192 ",
|
|
17499
|
+
"return ": "\u0280 ",
|
|
17500
|
+
"const ": "\u1D04 ",
|
|
17501
|
+
"export ": "\u1D07 ",
|
|
17502
|
+
"import ": "\u026A ",
|
|
17503
|
+
"interface ": "\u026A\u0274\u1D1B ",
|
|
17504
|
+
"class ": "\u1D04\u029Fs ",
|
|
17505
|
+
"async ": "\u1D00 ",
|
|
17506
|
+
"await ": "\u1D00\u1D21 ",
|
|
17507
|
+
"undefined": "\u1D1C\u0274\u1D05",
|
|
17508
|
+
"null": "\u0274\u1D1C\u029F",
|
|
17509
|
+
"true": "\u1D1B",
|
|
17510
|
+
"false": "\uA730",
|
|
17511
|
+
"```typescript": "```ts",
|
|
17512
|
+
"```javascript": "```js",
|
|
17513
|
+
"## ": "\u2E2B ",
|
|
17514
|
+
"### ": "\u2E2C ",
|
|
17515
|
+
"#### ": "\u2E2D ",
|
|
17516
|
+
'"description"': '"desc"',
|
|
17517
|
+
'"dependencies"': '"deps"',
|
|
17518
|
+
'"devDependencies"': '"devDeps"',
|
|
17519
|
+
'"repository"': '"repo"',
|
|
17520
|
+
'"homepage"': '"home"',
|
|
17521
|
+
'"keywords"': '"keys"',
|
|
17522
|
+
'"license"': '"lic"',
|
|
17523
|
+
'"version"': '"ver"',
|
|
17524
|
+
'"required"': '"req"',
|
|
17525
|
+
'"optional"': '"opt"',
|
|
17526
|
+
'"default"': '"def"',
|
|
17527
|
+
'"example"': '"ex"',
|
|
17528
|
+
'"properties"': '"props"',
|
|
17529
|
+
'"additionalProperties"': '"addProps"',
|
|
17530
|
+
"node_modules/": "nm/",
|
|
17531
|
+
"src/": "s/",
|
|
17532
|
+
"dist/": "d/",
|
|
17533
|
+
"test/": "t/",
|
|
17534
|
+
"tests/": "t/",
|
|
17535
|
+
".typescript": ".ts",
|
|
17536
|
+
".javascript": ".js"
|
|
17537
|
+
};
|
|
17538
|
+
/**
|
|
17539
|
+
* Compress text for token-efficient context loading.
|
|
17540
|
+
* Finds repeated substrings, replaces with paragraph-sign codes, generates a legend.
|
|
17541
|
+
* Inspired by the CTON compress-for-context tool.
|
|
17542
|
+
*/
|
|
17543
|
+
compressForContext(text, options) {
|
|
17544
|
+
const level = options?.level ?? "medium";
|
|
17545
|
+
let compressed = text;
|
|
17546
|
+
const legend = {};
|
|
17547
|
+
if (level !== "light") {
|
|
17548
|
+
compressed = compressed.replace(/[ \t]+/g, " ");
|
|
17549
|
+
compressed = compressed.replace(/\n{3,}/g, "\n\n");
|
|
17550
|
+
}
|
|
17551
|
+
if (level === "aggressive") {
|
|
17552
|
+
const patternResult = this.applyCommonPatterns(compressed);
|
|
17553
|
+
compressed = patternResult.text;
|
|
17554
|
+
Object.assign(legend, patternResult.legend);
|
|
17555
|
+
}
|
|
17556
|
+
const minLength2 = level === "light" ? 8 : level === "medium" ? 6 : 5;
|
|
17557
|
+
const minOccurrences = level === "light" ? 4 : 3;
|
|
17558
|
+
const maxSubstrings = level === "light" ? 20 : level === "medium" ? 30 : 36;
|
|
17559
|
+
const substrings = this.findRepeatedSubstrings(compressed, minLength2, minOccurrences, maxSubstrings);
|
|
17560
|
+
if (substrings.length > 0) {
|
|
17561
|
+
const totalSavings = substrings.reduce((sum, s) => sum + s.savings, 0);
|
|
17562
|
+
if (totalSavings > 5) {
|
|
17563
|
+
const result = this.applySubstringCompression(compressed, substrings);
|
|
17564
|
+
Object.assign(legend, result.legend);
|
|
17565
|
+
compressed = result.compressed;
|
|
17566
|
+
}
|
|
17567
|
+
}
|
|
17568
|
+
if (Object.keys(legend).length > 0) {
|
|
17569
|
+
const legendStr = "=== Legend ===\n" + Object.entries(legend).map(([a, f]) => `${a} = ${f}`).join("\n") + "\n=============\n\n";
|
|
17570
|
+
compressed = legendStr + compressed;
|
|
17571
|
+
}
|
|
17572
|
+
const originalTokens = this.estimateStringTokens(text);
|
|
17573
|
+
const hasLegend = Object.keys(legend).length > 0;
|
|
17574
|
+
const compressedTokens = hasLegend ? this.estimateStringTokens(compressed) : originalTokens;
|
|
17575
|
+
const savedTokens = Math.max(0, originalTokens - compressedTokens);
|
|
17576
|
+
return {
|
|
17577
|
+
compressed: hasLegend ? compressed : text,
|
|
17578
|
+
legend,
|
|
17579
|
+
stats: {
|
|
17580
|
+
originalTokens,
|
|
17581
|
+
compressedTokens,
|
|
17582
|
+
savedTokens,
|
|
17583
|
+
savedPercent: originalTokens > 0 ? Math.round(savedTokens / originalTokens * 100) : 0
|
|
17584
|
+
}
|
|
16778
17585
|
};
|
|
16779
|
-
this.defaultPromptTemplate = `## {name} ({type})
|
|
16780
|
-
{observations}
|
|
16781
|
-
{metadata}`;
|
|
16782
17586
|
}
|
|
16783
|
-
// ==================== Prompt Format ====================
|
|
16784
17587
|
/**
|
|
16785
|
-
*
|
|
16786
|
-
*
|
|
17588
|
+
* Compress entities for context loading. Formats entities as compact text,
|
|
17589
|
+
* then applies compression. Sorted by importance descending.
|
|
17590
|
+
*/
|
|
17591
|
+
compressEntitiesForContext(entities, options) {
|
|
17592
|
+
const sorted = [...entities].filter((e) => e.entityType !== "profile" && e.entityType !== "diary").sort((a, b) => (b.importance ?? 0) - (a.importance ?? 0));
|
|
17593
|
+
const lines = [];
|
|
17594
|
+
let tokenCount = 0;
|
|
17595
|
+
let entityCount = 0;
|
|
17596
|
+
const maxTokens = options?.maxTokens ?? Infinity;
|
|
17597
|
+
for (const e of sorted) {
|
|
17598
|
+
const obs = e.observations?.slice(0, 3).join("; ") ?? "";
|
|
17599
|
+
const line = `[${e.entityType}] ${e.name}: ${obs}`;
|
|
17600
|
+
const lineTokens = this.estimateStringTokens(line);
|
|
17601
|
+
if (tokenCount + lineTokens > maxTokens) break;
|
|
17602
|
+
lines.push(line);
|
|
17603
|
+
tokenCount += lineTokens;
|
|
17604
|
+
entityCount++;
|
|
17605
|
+
}
|
|
17606
|
+
const raw = lines.join("\n");
|
|
17607
|
+
const result = this.compressForContext(raw, { level: options?.level });
|
|
17608
|
+
return { ...result, entityCount };
|
|
17609
|
+
}
|
|
17610
|
+
// ==================== Compression Helpers ====================
|
|
17611
|
+
/**
|
|
17612
|
+
* Find repeated substrings and calculate compression potential.
|
|
17613
|
+
* Returns substrings sorted by net savings (highest first).
|
|
17614
|
+
* @internal
|
|
17615
|
+
*/
|
|
17616
|
+
findRepeatedSubstrings(text, minLength2, minOccurrences, maxSubstrings = 36) {
|
|
17617
|
+
if (text.length < minLength2 * 2) return [];
|
|
17618
|
+
const substringCounts = /* @__PURE__ */ new Map();
|
|
17619
|
+
const MAX_MAP_SIZE = 1e4;
|
|
17620
|
+
const tokens = text.split(/(\s+|[{}()\[\]<>:;,."'`|=])/);
|
|
17621
|
+
outer: for (let n = 1; n <= 6; n++) {
|
|
17622
|
+
for (let i = 0; i <= tokens.length - n; i++) {
|
|
17623
|
+
const ngram = tokens.slice(i, i + n).join("");
|
|
17624
|
+
if (ngram.length < minLength2 || ngram.length > 50) continue;
|
|
17625
|
+
if (/^\s*$/.test(ngram)) continue;
|
|
17626
|
+
if ((ngram.match(/\s/g) || []).length > ngram.length * 0.5) continue;
|
|
17627
|
+
const opens = (ngram.match(/[{(\[<]/g) || []).length;
|
|
17628
|
+
const closes = (ngram.match(/[})\]>]/g) || []).length;
|
|
17629
|
+
if (opens !== closes) continue;
|
|
17630
|
+
substringCounts.set(ngram, (substringCounts.get(ngram) || 0) + 1);
|
|
17631
|
+
if (substringCounts.size >= MAX_MAP_SIZE) break outer;
|
|
17632
|
+
}
|
|
17633
|
+
}
|
|
17634
|
+
const pathRe = /[a-zA-Z0-9_\-./]+\/[a-zA-Z0-9_\-./]+/g;
|
|
17635
|
+
let pathMatch;
|
|
17636
|
+
while ((pathMatch = pathRe.exec(text)) !== null) {
|
|
17637
|
+
const p = pathMatch[0];
|
|
17638
|
+
if (p.length >= minLength2 && substringCounts.size < MAX_MAP_SIZE) {
|
|
17639
|
+
substringCounts.set(p, (substringCounts.get(p) || 0) + 1);
|
|
17640
|
+
}
|
|
17641
|
+
}
|
|
17642
|
+
const candidates = [];
|
|
17643
|
+
for (const [substring, _mapCount] of substringCounts.entries()) {
|
|
17644
|
+
const actualCount = text.split(substring).length - 1;
|
|
17645
|
+
if (actualCount >= minOccurrences) {
|
|
17646
|
+
const abbrevLength = 2;
|
|
17647
|
+
const legendCost = abbrevLength + substring.length + 4;
|
|
17648
|
+
const savingsPerOccurrence = substring.length - abbrevLength;
|
|
17649
|
+
const netSavings = savingsPerOccurrence * actualCount - legendCost;
|
|
17650
|
+
if (netSavings > 5) {
|
|
17651
|
+
candidates.push({ substring, count: actualCount, savings: netSavings });
|
|
17652
|
+
}
|
|
17653
|
+
}
|
|
17654
|
+
}
|
|
17655
|
+
candidates.sort((a, b) => b.savings - a.savings);
|
|
17656
|
+
const selected = [];
|
|
17657
|
+
const usedSubstrings = [];
|
|
17658
|
+
for (const candidate of candidates) {
|
|
17659
|
+
let isTooSimilar = false;
|
|
17660
|
+
const candidateTrimmed = candidate.substring.trim();
|
|
17661
|
+
if (candidateTrimmed.length < 3) continue;
|
|
17662
|
+
for (const used of usedSubstrings) {
|
|
17663
|
+
const usedTrimmed = used.trim();
|
|
17664
|
+
if (used.includes(candidate.substring) || candidate.substring.includes(used)) {
|
|
17665
|
+
isTooSimilar = true;
|
|
17666
|
+
break;
|
|
17667
|
+
}
|
|
17668
|
+
if (candidateTrimmed === usedTrimmed || candidateTrimmed.includes(usedTrimmed) || usedTrimmed.includes(candidateTrimmed)) {
|
|
17669
|
+
isTooSimilar = true;
|
|
17670
|
+
break;
|
|
17671
|
+
}
|
|
17672
|
+
const shorter = candidateTrimmed.length < usedTrimmed.length ? candidateTrimmed : usedTrimmed;
|
|
17673
|
+
const longer = candidateTrimmed.length >= usedTrimmed.length ? candidateTrimmed : usedTrimmed;
|
|
17674
|
+
if (longer.includes(shorter.slice(0, Math.floor(shorter.length * 0.7)))) {
|
|
17675
|
+
isTooSimilar = true;
|
|
17676
|
+
break;
|
|
17677
|
+
}
|
|
17678
|
+
}
|
|
17679
|
+
if (!isTooSimilar) {
|
|
17680
|
+
selected.push(candidate);
|
|
17681
|
+
usedSubstrings.push(candidate.substring);
|
|
17682
|
+
if (selected.length >= maxSubstrings) break;
|
|
17683
|
+
}
|
|
17684
|
+
}
|
|
17685
|
+
return selected;
|
|
17686
|
+
}
|
|
17687
|
+
/**
|
|
17688
|
+
* Apply substring replacements to text.
|
|
17689
|
+
* Applies replacements in savings-descending order (as returned by findRepeatedSubstrings).
|
|
17690
|
+
* @internal
|
|
17691
|
+
*/
|
|
17692
|
+
applySubstringCompression(text, substrings) {
|
|
17693
|
+
const legend = {};
|
|
17694
|
+
let compressed = text;
|
|
17695
|
+
substrings.forEach((item, index) => {
|
|
17696
|
+
const abbrev = `\xA7${index.toString(36)}`;
|
|
17697
|
+
legend[abbrev] = item.substring;
|
|
17698
|
+
compressed = compressed.split(item.substring).join(abbrev);
|
|
17699
|
+
});
|
|
17700
|
+
return { compressed, legend };
|
|
17701
|
+
}
|
|
17702
|
+
/**
|
|
17703
|
+
* Apply COMMON_PATTERNS unicode abbreviations (aggressive level only).
|
|
17704
|
+
* @internal
|
|
17705
|
+
*/
|
|
17706
|
+
applyCommonPatterns(text) {
|
|
17707
|
+
let result = text;
|
|
17708
|
+
const legend = {};
|
|
17709
|
+
for (const [pattern2, replacement] of Object.entries(_ContextWindowManager.COMMON_PATTERNS)) {
|
|
17710
|
+
try {
|
|
17711
|
+
if (!result.includes(pattern2)) continue;
|
|
17712
|
+
const count = result.split(pattern2).length - 1;
|
|
17713
|
+
const savings = (pattern2.length - replacement.length) * count;
|
|
17714
|
+
if (savings > pattern2.length + replacement.length + 5) {
|
|
17715
|
+
legend[replacement] = pattern2;
|
|
17716
|
+
result = result.split(pattern2).join(replacement);
|
|
17717
|
+
}
|
|
17718
|
+
} catch {
|
|
17719
|
+
continue;
|
|
17720
|
+
}
|
|
17721
|
+
}
|
|
17722
|
+
return { text: result, legend };
|
|
17723
|
+
}
|
|
17724
|
+
};
|
|
17725
|
+
}
|
|
17726
|
+
});
|
|
17727
|
+
|
|
17728
|
+
// src/agent/MemoryFormatter.ts
|
|
17729
|
+
var MemoryFormatter;
|
|
17730
|
+
var init_MemoryFormatter = __esm({
|
|
17731
|
+
"src/agent/MemoryFormatter.ts"() {
|
|
17732
|
+
"use strict";
|
|
17733
|
+
init_esm_shims();
|
|
17734
|
+
MemoryFormatter = class {
|
|
17735
|
+
config;
|
|
17736
|
+
defaultPromptTemplate;
|
|
17737
|
+
constructor(config = {}) {
|
|
17738
|
+
this.config = {
|
|
17739
|
+
defaultMaxTokens: config.defaultMaxTokens ?? 2e3,
|
|
17740
|
+
tokenMultiplier: config.tokenMultiplier ?? 1.3,
|
|
17741
|
+
includeTimestamps: config.includeTimestamps ?? true,
|
|
17742
|
+
includeSalience: config.includeSalience ?? false,
|
|
17743
|
+
includeMemoryType: config.includeMemoryType ?? true,
|
|
17744
|
+
promptTemplate: config.promptTemplate ?? ""
|
|
17745
|
+
};
|
|
17746
|
+
this.defaultPromptTemplate = `## {name} ({type})
|
|
17747
|
+
{observations}
|
|
17748
|
+
{metadata}`;
|
|
17749
|
+
}
|
|
17750
|
+
// ==================== Prompt Format ====================
|
|
17751
|
+
/**
|
|
17752
|
+
* Format memories as text for LLM prompts.
|
|
17753
|
+
*
|
|
16787
17754
|
* @param memories - Memories to format
|
|
16788
17755
|
* @param options - Formatting options
|
|
16789
17756
|
* @returns Formatted text string
|
|
@@ -17711,6 +18678,7 @@ var init_WorkingMemoryManager = __esm({
|
|
|
17711
18678
|
});
|
|
17712
18679
|
|
|
17713
18680
|
// src/agent/SessionManager.ts
|
|
18681
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
17714
18682
|
var SessionManager;
|
|
17715
18683
|
var init_SessionManager = __esm({
|
|
17716
18684
|
"src/agent/SessionManager.ts"() {
|
|
@@ -17744,7 +18712,7 @@ var init_SessionManager = __esm({
|
|
|
17744
18712
|
*/
|
|
17745
18713
|
generateSessionId() {
|
|
17746
18714
|
const timestamp = Date.now();
|
|
17747
|
-
const random =
|
|
18715
|
+
const random = randomBytes3(4).toString("hex");
|
|
17748
18716
|
return `session_${timestamp}_${random}`;
|
|
17749
18717
|
}
|
|
17750
18718
|
// ==================== Session Creation ====================
|
|
@@ -18166,6 +19134,7 @@ var init_SessionManager = __esm({
|
|
|
18166
19134
|
});
|
|
18167
19135
|
|
|
18168
19136
|
// src/agent/EpisodicMemoryManager.ts
|
|
19137
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
18169
19138
|
var EpisodicRelations, EpisodicMemoryManager;
|
|
18170
19139
|
var init_EpisodicMemoryManager = __esm({
|
|
18171
19140
|
"src/agent/EpisodicMemoryManager.ts"() {
|
|
@@ -18205,7 +19174,7 @@ var init_EpisodicMemoryManager = __esm({
|
|
|
18205
19174
|
async createEpisode(content, options) {
|
|
18206
19175
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18207
19176
|
const timestamp = Date.now();
|
|
18208
|
-
const name = `episode_${timestamp}_${
|
|
19177
|
+
const name = `episode_${timestamp}_${randomBytes4(4).toString("hex")}`;
|
|
18209
19178
|
const entity = {
|
|
18210
19179
|
name,
|
|
18211
19180
|
entityType: options?.entityType ?? "episode",
|
|
@@ -20044,35 +21013,41 @@ var init_VisibilityResolver = __esm({
|
|
|
20044
21013
|
* @param requestingAgentId - ID of the agent requesting access
|
|
20045
21014
|
* @param requestingMeta - Metadata for the requesting agent (undefined = unregistered)
|
|
20046
21015
|
* @param ownerMeta - Metadata for the owning agent (undefined = unknown owner)
|
|
21016
|
+
* @param now - Override for the current time (ISO 8601). Defaults
|
|
21017
|
+
* to `new Date().toISOString()`. Useful for tests
|
|
21018
|
+
* and for evaluating access at a hypothetical time.
|
|
20047
21019
|
* @returns True if access is permitted
|
|
20048
21020
|
*/
|
|
20049
|
-
canAccess(memory, requestingAgentId, requestingMeta, ownerMeta) {
|
|
21021
|
+
canAccess(memory, requestingAgentId, requestingMeta, ownerMeta, now) {
|
|
21022
|
+
const currentTime = now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
21023
|
+
if (memory.visibleFrom && memory.visibleFrom > currentTime) return false;
|
|
21024
|
+
if (memory.visibleUntil && memory.visibleUntil < currentTime) return false;
|
|
20050
21025
|
if (memory.agentId === requestingAgentId) {
|
|
20051
21026
|
return true;
|
|
20052
21027
|
}
|
|
20053
21028
|
const visibility = memory.visibility ?? "private";
|
|
21029
|
+
let levelGrant = false;
|
|
20054
21030
|
if (visibility === "public") {
|
|
20055
|
-
|
|
20056
|
-
}
|
|
20057
|
-
if (!requestingMeta) {
|
|
21031
|
+
levelGrant = true;
|
|
21032
|
+
} else if (!requestingMeta) {
|
|
20058
21033
|
return false;
|
|
20059
|
-
}
|
|
20060
|
-
|
|
20061
|
-
|
|
20062
|
-
}
|
|
20063
|
-
if (visibility === "org") {
|
|
21034
|
+
} else if (visibility === "shared") {
|
|
21035
|
+
levelGrant = true;
|
|
21036
|
+
} else if (visibility === "org") {
|
|
20064
21037
|
const requesterOrg = requestingMeta.groupMembership?.org;
|
|
20065
21038
|
const ownerOrg = ownerMeta?.groupMembership?.org;
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
}
|
|
20069
|
-
if (visibility === "team") {
|
|
21039
|
+
levelGrant = !!requesterOrg && !!ownerOrg && requesterOrg === ownerOrg;
|
|
21040
|
+
} else if (visibility === "team") {
|
|
20070
21041
|
const requesterTeams = requestingMeta.groupMembership?.teams ?? [];
|
|
20071
21042
|
const ownerTeams = ownerMeta?.groupMembership?.teams ?? [];
|
|
20072
|
-
|
|
20073
|
-
return requesterTeams.some((t) => ownerTeams.includes(t));
|
|
21043
|
+
levelGrant = requesterTeams.length > 0 && ownerTeams.length > 0 && requesterTeams.some((t) => ownerTeams.includes(t));
|
|
20074
21044
|
}
|
|
20075
|
-
return false;
|
|
21045
|
+
if (!levelGrant) return false;
|
|
21046
|
+
if (memory.allowedRoles && memory.allowedRoles.length > 0) {
|
|
21047
|
+
const role = requestingMeta?.role;
|
|
21048
|
+
if (!role || !memory.allowedRoles.includes(role)) return false;
|
|
21049
|
+
}
|
|
21050
|
+
return true;
|
|
20076
21051
|
}
|
|
20077
21052
|
};
|
|
20078
21053
|
}
|
|
@@ -21119,6 +22094,7 @@ var init_SessionCheckpoint = __esm({
|
|
|
21119
22094
|
});
|
|
21120
22095
|
|
|
21121
22096
|
// src/agent/WorkThreadManager.ts
|
|
22097
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
21122
22098
|
var VALID_TRANSITIONS, CHILD_OF_RELATION, BLOCKED_BY_RELATION, WORK_THREAD_ENTITY_TYPE, WorkThreadManager;
|
|
21123
22099
|
var init_WorkThreadManager = __esm({
|
|
21124
22100
|
"src/agent/WorkThreadManager.ts"() {
|
|
@@ -21192,7 +22168,7 @@ var init_WorkThreadManager = __esm({
|
|
|
21192
22168
|
throw new Error(`Priority must be between 0 and 10, got ${options.priority}`);
|
|
21193
22169
|
}
|
|
21194
22170
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21195
|
-
const id = `thread_${Date.now()}_${
|
|
22171
|
+
const id = `thread_${Date.now()}_${randomBytes5(4).toString("hex")}`;
|
|
21196
22172
|
const thread = {
|
|
21197
22173
|
id,
|
|
21198
22174
|
title,
|
|
@@ -22920,8 +23896,9 @@ var init_artifact = __esm({
|
|
|
22920
23896
|
});
|
|
22921
23897
|
|
|
22922
23898
|
// src/agent/ArtifactManager.ts
|
|
23899
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
22923
23900
|
function generateShortId() {
|
|
22924
|
-
return
|
|
23901
|
+
return randomBytes6(2).toString("hex");
|
|
22925
23902
|
}
|
|
22926
23903
|
function formatDateUTC(date) {
|
|
22927
23904
|
const y = date.getUTCFullYear();
|
|
@@ -23707,152 +24684,2289 @@ var init_SemanticForget = __esm({
|
|
|
23707
24684
|
}
|
|
23708
24685
|
});
|
|
23709
24686
|
|
|
23710
|
-
// src/
|
|
23711
|
-
import
|
|
23712
|
-
|
|
23713
|
-
|
|
23714
|
-
|
|
24687
|
+
// src/agent/MemoryEngine.ts
|
|
24688
|
+
import { EventEmitter as EventEmitter6 } from "events";
|
|
24689
|
+
import { createHash } from "crypto";
|
|
24690
|
+
function stripRolePrefix(text) {
|
|
24691
|
+
return text.replace(ROLE_PREFIX_RE, "");
|
|
24692
|
+
}
|
|
24693
|
+
function longestCommonPrefix(a, b) {
|
|
24694
|
+
const max2 = Math.min(a.length, b.length);
|
|
24695
|
+
let i = 0;
|
|
24696
|
+
while (i < max2 && a[i] === b[i]) i += 1;
|
|
24697
|
+
return a.slice(0, i);
|
|
24698
|
+
}
|
|
24699
|
+
function tokeniseForDedup(text) {
|
|
24700
|
+
return new Set(
|
|
24701
|
+
text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((t) => t.length > 0)
|
|
24702
|
+
);
|
|
24703
|
+
}
|
|
24704
|
+
function hasStoreEmbedding(storage) {
|
|
24705
|
+
return typeof storage?.storeEmbedding === "function";
|
|
24706
|
+
}
|
|
24707
|
+
var ROLE_PREFIX_RE, MemoryEngine;
|
|
24708
|
+
var init_MemoryEngine = __esm({
|
|
24709
|
+
"src/agent/MemoryEngine.ts"() {
|
|
23715
24710
|
"use strict";
|
|
23716
24711
|
init_esm_shims();
|
|
23717
|
-
|
|
23718
|
-
|
|
23719
|
-
|
|
23720
|
-
|
|
23721
|
-
|
|
23722
|
-
|
|
23723
|
-
|
|
23724
|
-
|
|
23725
|
-
|
|
23726
|
-
|
|
23727
|
-
|
|
23728
|
-
|
|
23729
|
-
init_TagManager();
|
|
23730
|
-
init_AnalyticsManager();
|
|
23731
|
-
init_CompressionManager();
|
|
23732
|
-
init_ArchiveManager();
|
|
23733
|
-
init_AutoLinker();
|
|
23734
|
-
init_FactExtractor();
|
|
23735
|
-
init_TransitionLedger();
|
|
23736
|
-
init_AccessTracker();
|
|
23737
|
-
init_DecayEngine();
|
|
23738
|
-
init_DecayScheduler();
|
|
23739
|
-
init_ConsolidationScheduler();
|
|
23740
|
-
init_SalienceEngine();
|
|
23741
|
-
init_ContextWindowManager();
|
|
23742
|
-
init_MemoryFormatter();
|
|
23743
|
-
init_AgentMemoryManager();
|
|
23744
|
-
init_ArtifactManager();
|
|
23745
|
-
init_DreamEngine();
|
|
23746
|
-
init_RefIndex();
|
|
23747
|
-
init_ObserverPipeline();
|
|
23748
|
-
init_constants();
|
|
23749
|
-
init_utils();
|
|
23750
|
-
init_ContradictionDetector();
|
|
23751
|
-
init_SemanticForget();
|
|
23752
|
-
ManagerContext = class {
|
|
23753
|
-
// Type as GraphStorage for manager compatibility; actual instance may be SQLiteStorage
|
|
23754
|
-
// which implements the same interface via duck typing
|
|
23755
|
-
storage;
|
|
23756
|
-
defaultProjectId;
|
|
23757
|
-
savedSearchesFilePath;
|
|
23758
|
-
tagAliasesFilePath;
|
|
23759
|
-
refIndexFilePath;
|
|
23760
|
-
_observerPipeline;
|
|
23761
|
-
// ==================== LAZY-INITIALIZED CORE MANAGERS ====================
|
|
23762
|
-
_entityManager;
|
|
23763
|
-
_relationManager;
|
|
23764
|
-
_observationManager;
|
|
23765
|
-
_hierarchyManager;
|
|
23766
|
-
_graphTraversal;
|
|
23767
|
-
_searchManager;
|
|
23768
|
-
_rankedSearch;
|
|
23769
|
-
_ioManager;
|
|
23770
|
-
_tagManager;
|
|
23771
|
-
_analyticsManager;
|
|
23772
|
-
_compressionManager;
|
|
23773
|
-
_archiveManager;
|
|
23774
|
-
_autoLinker;
|
|
23775
|
-
_factExtractor;
|
|
23776
|
-
_transitionLedger;
|
|
23777
|
-
_semanticSearch;
|
|
23778
|
-
_accessTracker;
|
|
23779
|
-
_decayEngine;
|
|
23780
|
-
_decayScheduler;
|
|
23781
|
-
_salienceEngine;
|
|
23782
|
-
_contextWindowManager;
|
|
23783
|
-
_memoryFormatter;
|
|
23784
|
-
_agentMemory;
|
|
23785
|
-
_refIndex;
|
|
23786
|
-
_artifactManager;
|
|
23787
|
-
_consolidationScheduler;
|
|
23788
|
-
_dreamEngine;
|
|
23789
|
-
_llmQueryPlanner;
|
|
23790
|
-
_llmSearchExecutor;
|
|
23791
|
-
_semanticForget;
|
|
23792
|
-
constructor(pathOrOptions) {
|
|
23793
|
-
const opts = typeof pathOrOptions === "string" ? { storagePath: pathOrOptions } : pathOrOptions;
|
|
23794
|
-
this.defaultProjectId = opts.defaultProjectId;
|
|
23795
|
-
const validatedPath = validateFilePath(opts.storagePath);
|
|
23796
|
-
const dir = path5.dirname(validatedPath);
|
|
23797
|
-
const basename2 = path5.basename(validatedPath, path5.extname(validatedPath));
|
|
23798
|
-
this.savedSearchesFilePath = path5.join(dir, `${basename2}-saved-searches.jsonl`);
|
|
23799
|
-
this.tagAliasesFilePath = path5.join(dir, `${basename2}-tag-aliases.jsonl`);
|
|
23800
|
-
this.refIndexFilePath = path5.join(dir, `${basename2}-ref-index.jsonl`);
|
|
23801
|
-
this.storage = createStorageFromPath(validatedPath);
|
|
23802
|
-
if (opts.enableContradictionDetection) {
|
|
23803
|
-
this.initContradictionDetection(opts.contradictionThreshold);
|
|
24712
|
+
ROLE_PREFIX_RE = /^\[role=[a-z]+\]\s*/i;
|
|
24713
|
+
MemoryEngine = class {
|
|
24714
|
+
events = new EventEmitter6();
|
|
24715
|
+
/** Dependencies bundle — populated in constructor, consumed in T5–T10. */
|
|
24716
|
+
deps;
|
|
24717
|
+
/** Resolved config — populated in constructor, consumed in T5–T10. */
|
|
24718
|
+
cfg;
|
|
24719
|
+
constructor(storage, entityManager, episodicMemory, workingMemory, importanceScorer, semanticSearch, embeddingService, config = {}) {
|
|
24720
|
+
if (config.semanticDedupEnabled && !semanticSearch) {
|
|
24721
|
+
throw new TypeError(
|
|
24722
|
+
"MemoryEngine: semanticDedupEnabled=true requires a SemanticSearch instance"
|
|
24723
|
+
);
|
|
23804
24724
|
}
|
|
24725
|
+
this.deps = {
|
|
24726
|
+
storage,
|
|
24727
|
+
entityManager,
|
|
24728
|
+
episodicMemory,
|
|
24729
|
+
workingMemory,
|
|
24730
|
+
importanceScorer,
|
|
24731
|
+
semanticSearch,
|
|
24732
|
+
embeddingService
|
|
24733
|
+
};
|
|
24734
|
+
this.cfg = {
|
|
24735
|
+
jaccardThreshold: config.jaccardThreshold ?? 0.72,
|
|
24736
|
+
prefixOverlapThreshold: config.prefixOverlapThreshold ?? 0.5,
|
|
24737
|
+
dedupScanWindow: config.dedupScanWindow ?? 200,
|
|
24738
|
+
maxTurnsPerSession: config.maxTurnsPerSession ?? 1e3,
|
|
24739
|
+
semanticDedupEnabled: config.semanticDedupEnabled ?? false,
|
|
24740
|
+
semanticThreshold: config.semanticThreshold ?? 0.92,
|
|
24741
|
+
recentTurnsForImportance: config.recentTurnsForImportance ?? 10
|
|
24742
|
+
};
|
|
23805
24743
|
}
|
|
23806
|
-
|
|
23807
|
-
|
|
23808
|
-
|
|
23809
|
-
|
|
23810
|
-
|
|
23811
|
-
|
|
23812
|
-
|
|
23813
|
-
|
|
23814
|
-
|
|
23815
|
-
|
|
23816
|
-
|
|
23817
|
-
|
|
23818
|
-
|
|
24744
|
+
async addTurn(content, options) {
|
|
24745
|
+
const dup = await this.checkDuplicate(content, options.sessionId);
|
|
24746
|
+
if (dup.isDuplicate && dup.match) {
|
|
24747
|
+
this.events.emit("memoryEngine:duplicateDetected", {
|
|
24748
|
+
existingEntity: dup.match,
|
|
24749
|
+
attemptedContent: content,
|
|
24750
|
+
sessionId: options.sessionId,
|
|
24751
|
+
tier: dup.tier
|
|
24752
|
+
});
|
|
24753
|
+
return {
|
|
24754
|
+
entity: dup.match,
|
|
24755
|
+
duplicateDetected: true,
|
|
24756
|
+
duplicateOf: dup.match.name,
|
|
24757
|
+
duplicateTier: dup.tier,
|
|
24758
|
+
importanceScore: dup.match.importance ?? 0
|
|
24759
|
+
};
|
|
24760
|
+
}
|
|
24761
|
+
let importance;
|
|
24762
|
+
if (typeof options.importance === "number") {
|
|
24763
|
+
importance = options.importance;
|
|
24764
|
+
} else {
|
|
24765
|
+
const recentTurns = options.recentTurns ?? await this.loadRecentTurnsForImportance(options.sessionId);
|
|
24766
|
+
importance = this.deps.importanceScorer.score(content, {
|
|
24767
|
+
queryContext: options.queryContext,
|
|
24768
|
+
recentTurns
|
|
24769
|
+
});
|
|
24770
|
+
}
|
|
24771
|
+
const observation = `[role=${options.role}] ${content}`;
|
|
24772
|
+
const entity = await this.deps.episodicMemory.createEpisode(observation, {
|
|
24773
|
+
sessionId: options.sessionId,
|
|
24774
|
+
agentId: options.agentId,
|
|
24775
|
+
importance
|
|
24776
|
+
});
|
|
24777
|
+
const hash = this.computeContentHash(content);
|
|
24778
|
+
await this.deps.storage.updateEntity(entity.name, { contentHash: hash });
|
|
24779
|
+
const enriched = { ...entity, contentHash: hash };
|
|
24780
|
+
if (this.deps.embeddingService && hasStoreEmbedding(this.deps.storage)) {
|
|
24781
|
+
try {
|
|
24782
|
+
const vector = await this.deps.embeddingService.embed(content);
|
|
24783
|
+
const model = this.deps.embeddingService.getModelName?.() ?? "unknown";
|
|
24784
|
+
this.deps.storage.storeEmbedding(entity.name, vector, model);
|
|
24785
|
+
} catch {
|
|
23819
24786
|
}
|
|
23820
|
-
const detector = new ContradictionDetector(ss, threshold ?? 0.85);
|
|
23821
|
-
this.observationManager.setContradictionDetector(detector, this.entityManager);
|
|
23822
|
-
} catch (err) {
|
|
23823
|
-
console.warn(
|
|
23824
|
-
"[ManagerContext] Could not initialise contradiction detection:",
|
|
23825
|
-
err instanceof Error ? err.message : String(err)
|
|
23826
|
-
);
|
|
23827
24787
|
}
|
|
24788
|
+
this.events.emit("memoryEngine:turnAdded", {
|
|
24789
|
+
entity: enriched,
|
|
24790
|
+
sessionId: options.sessionId,
|
|
24791
|
+
role: options.role,
|
|
24792
|
+
importance
|
|
24793
|
+
});
|
|
24794
|
+
return { entity: enriched, duplicateDetected: false, importanceScore: importance };
|
|
23828
24795
|
}
|
|
23829
|
-
|
|
23830
|
-
|
|
23831
|
-
|
|
23832
|
-
|
|
23833
|
-
this.storage,
|
|
23834
|
-
{ defaultProjectId: this.defaultProjectId }
|
|
24796
|
+
async loadRecentTurnsForImportance(sessionId) {
|
|
24797
|
+
const recent = await this.getRecentSessionEntities(
|
|
24798
|
+
sessionId,
|
|
24799
|
+
this.cfg.recentTurnsForImportance
|
|
23835
24800
|
);
|
|
24801
|
+
return recent.map((e) => stripRolePrefix(e.observations[0] ?? ""));
|
|
23836
24802
|
}
|
|
23837
|
-
|
|
23838
|
-
|
|
23839
|
-
|
|
23840
|
-
|
|
23841
|
-
|
|
23842
|
-
|
|
23843
|
-
|
|
24803
|
+
async getSessionTurns(sessionId, options = {}) {
|
|
24804
|
+
const graph = await this.deps.storage.loadGraph();
|
|
24805
|
+
let turns = graph.entities.filter(
|
|
24806
|
+
(e) => e.sessionId === sessionId
|
|
24807
|
+
);
|
|
24808
|
+
turns.sort((a, b) => {
|
|
24809
|
+
const aT = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
|
24810
|
+
const bT = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
|
24811
|
+
return aT - bT;
|
|
24812
|
+
});
|
|
24813
|
+
if (options.role) {
|
|
24814
|
+
const prefix = `[role=${options.role}]`;
|
|
24815
|
+
turns = turns.filter((e) => (e.observations[0] ?? "").startsWith(prefix));
|
|
24816
|
+
}
|
|
24817
|
+
if (typeof options.limit === "number") {
|
|
24818
|
+
turns = turns.slice(0, options.limit);
|
|
24819
|
+
}
|
|
24820
|
+
return turns;
|
|
24821
|
+
}
|
|
24822
|
+
async checkDuplicate(content, sessionId) {
|
|
24823
|
+
if (this.cfg.semanticDedupEnabled && this.deps.semanticSearch) {
|
|
24824
|
+
const ts = await this.checkTierSemantic(content, sessionId);
|
|
24825
|
+
if (ts.isDuplicate) return ts;
|
|
24826
|
+
}
|
|
24827
|
+
const t1 = await this.checkTierExact(content, sessionId);
|
|
24828
|
+
if (t1.isDuplicate) return t1;
|
|
24829
|
+
const recent = await this.getRecentSessionEntities(sessionId, this.cfg.dedupScanWindow);
|
|
24830
|
+
const t2 = this.checkTierPrefix(content, recent);
|
|
24831
|
+
if (t2.isDuplicate) return t2;
|
|
24832
|
+
const t3 = this.checkTierJaccard(content, recent);
|
|
24833
|
+
if (t3.isDuplicate) return t3;
|
|
24834
|
+
return { isDuplicate: false };
|
|
24835
|
+
}
|
|
24836
|
+
async checkTierSemantic(content, sessionId) {
|
|
24837
|
+
if (!this.deps.semanticSearch) return { isDuplicate: false };
|
|
24838
|
+
const graph = await this.deps.storage.loadGraph();
|
|
24839
|
+
const results = await this.deps.semanticSearch.search(graph, content, 5, this.cfg.semanticThreshold);
|
|
24840
|
+
for (const hit of results) {
|
|
24841
|
+
if (hit.similarity < this.cfg.semanticThreshold) continue;
|
|
24842
|
+
const candidate = hit.entity;
|
|
24843
|
+
if (candidate.sessionId === sessionId) {
|
|
24844
|
+
return { isDuplicate: true, match: candidate, tier: "semantic" };
|
|
24845
|
+
}
|
|
24846
|
+
}
|
|
24847
|
+
return { isDuplicate: false };
|
|
24848
|
+
}
|
|
24849
|
+
computeContentHash(content) {
|
|
24850
|
+
return createHash("sha256").update(content).digest("hex");
|
|
24851
|
+
}
|
|
24852
|
+
async checkTierExact(content, sessionId) {
|
|
24853
|
+
const hash = this.computeContentHash(content);
|
|
24854
|
+
const graph = await this.deps.storage.loadGraph();
|
|
24855
|
+
const candidates = graph.entities.filter(
|
|
24856
|
+
(e) => e.contentHash === hash
|
|
24857
|
+
);
|
|
24858
|
+
const match = candidates.find((e) => e.sessionId === sessionId);
|
|
24859
|
+
if (match) return { isDuplicate: true, match, tier: "exact" };
|
|
24860
|
+
return { isDuplicate: false };
|
|
24861
|
+
}
|
|
24862
|
+
async getRecentSessionEntities(sessionId, windowSize) {
|
|
24863
|
+
const graph = await this.deps.storage.loadGraph();
|
|
24864
|
+
const sessionEntities = graph.entities.filter(
|
|
24865
|
+
(e) => e.sessionId === sessionId
|
|
24866
|
+
);
|
|
24867
|
+
sessionEntities.sort((a, b) => {
|
|
24868
|
+
const aT = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
|
24869
|
+
const bT = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
|
24870
|
+
return bT - aT;
|
|
24871
|
+
});
|
|
24872
|
+
return sessionEntities.slice(0, windowSize);
|
|
24873
|
+
}
|
|
24874
|
+
checkTierPrefix(content, candidates) {
|
|
24875
|
+
for (const candidate of candidates) {
|
|
24876
|
+
const candidateContent = stripRolePrefix(candidate.observations[0] ?? "");
|
|
24877
|
+
const shared = longestCommonPrefix(content, candidateContent);
|
|
24878
|
+
const ratio = shared.length / Math.max(content.length, candidateContent.length);
|
|
24879
|
+
if (ratio >= this.cfg.prefixOverlapThreshold) {
|
|
24880
|
+
return { isDuplicate: true, match: candidate, tier: "prefix" };
|
|
24881
|
+
}
|
|
24882
|
+
}
|
|
24883
|
+
return { isDuplicate: false };
|
|
24884
|
+
}
|
|
24885
|
+
checkTierJaccard(content, candidates) {
|
|
24886
|
+
const contentTokens = tokeniseForDedup(content);
|
|
24887
|
+
if (contentTokens.size === 0) return { isDuplicate: false };
|
|
24888
|
+
for (const candidate of candidates) {
|
|
24889
|
+
const candidateContent = stripRolePrefix(candidate.observations[0] ?? "");
|
|
24890
|
+
const candidateTokens = tokeniseForDedup(candidateContent);
|
|
24891
|
+
if (candidateTokens.size === 0) continue;
|
|
24892
|
+
let intersection = 0;
|
|
24893
|
+
for (const token of contentTokens) {
|
|
24894
|
+
if (candidateTokens.has(token)) intersection += 1;
|
|
24895
|
+
}
|
|
24896
|
+
const union = contentTokens.size + candidateTokens.size - intersection;
|
|
24897
|
+
const jaccard3 = union === 0 ? 0 : intersection / union;
|
|
24898
|
+
if (jaccard3 >= this.cfg.jaccardThreshold) {
|
|
24899
|
+
return { isDuplicate: true, match: candidate, tier: "jaccard" };
|
|
24900
|
+
}
|
|
24901
|
+
}
|
|
24902
|
+
return { isDuplicate: false };
|
|
24903
|
+
}
|
|
24904
|
+
async deleteSession(sessionId) {
|
|
24905
|
+
const turns = await this.getSessionTurns(sessionId);
|
|
24906
|
+
if (turns.length === 0) return { deleted: 0 };
|
|
24907
|
+
const names = turns.map((t) => t.name);
|
|
24908
|
+
await this.deps.entityManager.deleteEntities(names);
|
|
24909
|
+
this.events.emit("memoryEngine:sessionDeleted", {
|
|
24910
|
+
sessionId,
|
|
24911
|
+
deletedCount: names.length
|
|
24912
|
+
});
|
|
24913
|
+
return { deleted: names.length };
|
|
23844
24914
|
}
|
|
23845
|
-
|
|
23846
|
-
|
|
23847
|
-
|
|
24915
|
+
async listSessions() {
|
|
24916
|
+
const graph = await this.deps.storage.loadGraph();
|
|
24917
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
24918
|
+
for (const e of graph.entities) {
|
|
24919
|
+
const s = e.sessionId;
|
|
24920
|
+
if (s) sessions.add(s);
|
|
24921
|
+
}
|
|
24922
|
+
return Array.from(sessions);
|
|
23848
24923
|
}
|
|
23849
|
-
|
|
23850
|
-
|
|
23851
|
-
|
|
24924
|
+
};
|
|
24925
|
+
}
|
|
24926
|
+
});
|
|
24927
|
+
|
|
24928
|
+
// src/agent/ImportanceScorer.ts
|
|
24929
|
+
function tokenise(text) {
|
|
24930
|
+
return new Set(
|
|
24931
|
+
text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((t) => t.length > 0)
|
|
24932
|
+
);
|
|
24933
|
+
}
|
|
24934
|
+
function countIntersection(a, b) {
|
|
24935
|
+
let count = 0;
|
|
24936
|
+
for (const token of a) {
|
|
24937
|
+
if (b.has(token)) count += 1;
|
|
24938
|
+
}
|
|
24939
|
+
return count;
|
|
24940
|
+
}
|
|
24941
|
+
var ImportanceScorer;
|
|
24942
|
+
var init_ImportanceScorer = __esm({
|
|
24943
|
+
"src/agent/ImportanceScorer.ts"() {
|
|
24944
|
+
"use strict";
|
|
24945
|
+
init_esm_shims();
|
|
24946
|
+
ImportanceScorer = class {
|
|
24947
|
+
domainKeywords;
|
|
24948
|
+
lengthWeight;
|
|
24949
|
+
keywordWeight;
|
|
24950
|
+
overlapWeight;
|
|
24951
|
+
constructor(config = {}) {
|
|
24952
|
+
this.domainKeywords = config.domainKeywords ?? /* @__PURE__ */ new Set();
|
|
24953
|
+
this.lengthWeight = config.lengthWeight ?? 0.3;
|
|
24954
|
+
this.keywordWeight = config.keywordWeight ?? 0.4;
|
|
24955
|
+
this.overlapWeight = config.overlapWeight ?? 0.3;
|
|
24956
|
+
}
|
|
24957
|
+
/**
|
|
24958
|
+
* Score new content at creation time.
|
|
24959
|
+
*
|
|
24960
|
+
* PRD MEM-02: "Auto-importance scoring evaluates: content length
|
|
24961
|
+
* (log-scaled), domain keyword presence, query token overlap with
|
|
24962
|
+
* recent turns" (PRD §8 line 409).
|
|
24963
|
+
*
|
|
24964
|
+
* Returns integer in [0, 10] (memoryjs scale). PRD's narrower [1.0, 3.0]
|
|
24965
|
+
* range is out of scope here; the Decay Extensions spec owns the mapping.
|
|
24966
|
+
*/
|
|
24967
|
+
score(content, options = {}) {
|
|
24968
|
+
if (content.length === 0) return 0;
|
|
24969
|
+
const contentTokens = tokenise(content);
|
|
24970
|
+
const lengthSignal = Math.min(1, Math.log10(content.length) / 4);
|
|
24971
|
+
const keywordSignal = this.domainKeywords.size > 0 ? countIntersection(contentTokens, this.domainKeywords) / this.domainKeywords.size : 0;
|
|
24972
|
+
const overlapCorpus = [];
|
|
24973
|
+
if (options.queryContext) overlapCorpus.push(options.queryContext);
|
|
24974
|
+
if (options.recentTurns) overlapCorpus.push(...options.recentTurns);
|
|
24975
|
+
let overlapSignal;
|
|
24976
|
+
if (overlapCorpus.length === 0) {
|
|
24977
|
+
overlapSignal = 0.5;
|
|
24978
|
+
} else {
|
|
24979
|
+
const corpusTokens = tokenise(overlapCorpus.join(" "));
|
|
24980
|
+
overlapSignal = contentTokens.size > 0 ? countIntersection(contentTokens, corpusTokens) / contentTokens.size : 0;
|
|
24981
|
+
}
|
|
24982
|
+
const raw = this.lengthWeight * lengthSignal + this.keywordWeight * keywordSignal + this.overlapWeight * overlapSignal;
|
|
24983
|
+
return Math.max(0, Math.min(10, Math.round(raw * 10)));
|
|
23852
24984
|
}
|
|
23853
|
-
|
|
23854
|
-
|
|
23855
|
-
|
|
24985
|
+
};
|
|
24986
|
+
}
|
|
24987
|
+
});
|
|
24988
|
+
|
|
24989
|
+
// src/agent/InMemoryBackend.ts
|
|
24990
|
+
function turnToEntity(turn) {
|
|
24991
|
+
const prd = turn.importance;
|
|
24992
|
+
const memoryjsImportance = Math.max(0, Math.min(10, (prd - 1) * 5));
|
|
24993
|
+
return {
|
|
24994
|
+
name: turn.id,
|
|
24995
|
+
entityType: "memory_turn",
|
|
24996
|
+
observations: [turn.content],
|
|
24997
|
+
createdAt: turn.createdAt,
|
|
24998
|
+
lastModified: turn.createdAt,
|
|
24999
|
+
lastAccessedAt: turn.lastAccessedAt,
|
|
25000
|
+
importance: memoryjsImportance,
|
|
25001
|
+
memoryType: "episodic",
|
|
25002
|
+
accessCount: turn.accessCount ?? 0,
|
|
25003
|
+
confidence: 1,
|
|
25004
|
+
confirmationCount: 0,
|
|
25005
|
+
visibility: "private",
|
|
25006
|
+
sessionId: turn.sessionId
|
|
25007
|
+
};
|
|
25008
|
+
}
|
|
25009
|
+
var InMemoryBackend;
|
|
25010
|
+
var init_InMemoryBackend = __esm({
|
|
25011
|
+
"src/agent/InMemoryBackend.ts"() {
|
|
25012
|
+
"use strict";
|
|
25013
|
+
init_esm_shims();
|
|
25014
|
+
InMemoryBackend = class {
|
|
25015
|
+
constructor(decayEngine) {
|
|
25016
|
+
this.decayEngine = decayEngine;
|
|
25017
|
+
}
|
|
25018
|
+
/** Per-session FIFO ordered list of turns. */
|
|
25019
|
+
turns = /* @__PURE__ */ new Map();
|
|
25020
|
+
async add(turn) {
|
|
25021
|
+
const list = this.turns.get(turn.sessionId) ?? [];
|
|
25022
|
+
if (list.some((existing) => existing.content === turn.content)) {
|
|
25023
|
+
return;
|
|
25024
|
+
}
|
|
25025
|
+
list.push(turn);
|
|
25026
|
+
this.turns.set(turn.sessionId, list);
|
|
25027
|
+
}
|
|
25028
|
+
async get_weighted(query, sessionId, options) {
|
|
25029
|
+
const list = this.turns.get(sessionId);
|
|
25030
|
+
if (!list || list.length === 0) return [];
|
|
25031
|
+
const threshold = options?.threshold ?? this.decayEngine.prdMinImportanceThreshold;
|
|
25032
|
+
const limit = options?.limit ?? 10;
|
|
25033
|
+
const scored = list.map((turn) => {
|
|
25034
|
+
const synthetic = turnToEntity(turn);
|
|
25035
|
+
const score = this.decayEngine.calculatePrdEffectiveImportance(
|
|
25036
|
+
synthetic,
|
|
25037
|
+
query
|
|
25038
|
+
);
|
|
25039
|
+
return { turn, score };
|
|
25040
|
+
});
|
|
25041
|
+
return scored.filter((wt) => wt.score >= threshold).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
25042
|
+
}
|
|
25043
|
+
async delete_session(sessionId) {
|
|
25044
|
+
this.turns.delete(sessionId);
|
|
25045
|
+
}
|
|
25046
|
+
async list_sessions() {
|
|
25047
|
+
return Array.from(this.turns.keys());
|
|
25048
|
+
}
|
|
25049
|
+
};
|
|
25050
|
+
}
|
|
25051
|
+
});
|
|
25052
|
+
|
|
25053
|
+
// src/agent/SQLiteBackend.ts
|
|
25054
|
+
function entityToTurn(entity) {
|
|
25055
|
+
const observation = entity.observations?.[0] ?? "";
|
|
25056
|
+
const m = observation.match(/^\[role=([a-z]+)\]\s*(.*)$/i);
|
|
25057
|
+
const role = m?.[1]?.toLowerCase() ?? "user";
|
|
25058
|
+
const content = m?.[2] ?? observation;
|
|
25059
|
+
const memoryjsImportance = entity.importance ?? 5;
|
|
25060
|
+
const prdImportance = 1 + memoryjsImportance / 10 * 2;
|
|
25061
|
+
return {
|
|
25062
|
+
id: entity.name,
|
|
25063
|
+
sessionId: entity.sessionId ?? "",
|
|
25064
|
+
content,
|
|
25065
|
+
role,
|
|
25066
|
+
importance: prdImportance,
|
|
25067
|
+
createdAt: entity.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
25068
|
+
lastAccessedAt: entity.lastAccessedAt,
|
|
25069
|
+
accessCount: entity.accessCount,
|
|
25070
|
+
embedding: void 0,
|
|
25071
|
+
// not exposed via getSessionTurns; future read-path enhancement
|
|
25072
|
+
metadata: void 0
|
|
25073
|
+
// see the metadata-round-trip caveat in the contract test
|
|
25074
|
+
};
|
|
25075
|
+
}
|
|
25076
|
+
var SQLiteBackend;
|
|
25077
|
+
var init_SQLiteBackend = __esm({
|
|
25078
|
+
"src/agent/SQLiteBackend.ts"() {
|
|
25079
|
+
"use strict";
|
|
25080
|
+
init_esm_shims();
|
|
25081
|
+
SQLiteBackend = class {
|
|
25082
|
+
constructor(memoryEngine, decayEngine, options = {}) {
|
|
25083
|
+
this.memoryEngine = memoryEngine;
|
|
25084
|
+
this.decayEngine = decayEngine;
|
|
25085
|
+
this.options = {
|
|
25086
|
+
dedupOnAdd: options.dedupOnAdd ?? true,
|
|
25087
|
+
preserveCallerIds: options.preserveCallerIds ?? false
|
|
25088
|
+
};
|
|
25089
|
+
}
|
|
25090
|
+
options;
|
|
25091
|
+
async add(turn) {
|
|
25092
|
+
if (!this.options.dedupOnAdd) {
|
|
25093
|
+
throw new Error(
|
|
25094
|
+
"SQLiteBackend: dedupOnAdd=false bypass path is not implemented yet. See docs/superpowers/specs/2026-04-16-memory-engine-decay-extensions-design.md"
|
|
25095
|
+
);
|
|
25096
|
+
}
|
|
25097
|
+
const memoryjsImportance = Math.max(0, Math.min(10, (turn.importance - 1) * 5));
|
|
25098
|
+
const result = await this.memoryEngine.addTurn(turn.content, {
|
|
25099
|
+
sessionId: turn.sessionId,
|
|
25100
|
+
role: turn.role,
|
|
25101
|
+
importance: memoryjsImportance
|
|
25102
|
+
});
|
|
25103
|
+
if (this.options.preserveCallerIds && !result.duplicateDetected && turn.id !== result.entity.name) {
|
|
25104
|
+
throw new Error(
|
|
25105
|
+
"SQLiteBackend: preserveCallerIds=true requires storage.renameEntity which is not implemented yet. Default false; see \u03B2.3 spec notes."
|
|
25106
|
+
);
|
|
25107
|
+
}
|
|
25108
|
+
}
|
|
25109
|
+
async get_weighted(query, sessionId, options) {
|
|
25110
|
+
const turns = await this.memoryEngine.getSessionTurns(sessionId);
|
|
25111
|
+
if (turns.length === 0) return [];
|
|
25112
|
+
const threshold = options?.threshold ?? this.decayEngine.prdMinImportanceThreshold;
|
|
25113
|
+
const limit = options?.limit ?? 10;
|
|
25114
|
+
const scored = turns.map((entity) => {
|
|
25115
|
+
const score = this.decayEngine.calculatePrdEffectiveImportance(entity, query);
|
|
25116
|
+
return { turn: entityToTurn(entity), score };
|
|
25117
|
+
});
|
|
25118
|
+
return scored.filter((wt) => wt.score >= threshold).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
25119
|
+
}
|
|
25120
|
+
async delete_session(sessionId) {
|
|
25121
|
+
await this.memoryEngine.deleteSession(sessionId);
|
|
25122
|
+
}
|
|
25123
|
+
async list_sessions() {
|
|
25124
|
+
return await this.memoryEngine.listSessions();
|
|
25125
|
+
}
|
|
25126
|
+
};
|
|
25127
|
+
}
|
|
25128
|
+
});
|
|
25129
|
+
|
|
25130
|
+
// src/agent/MemoryValidator.ts
|
|
25131
|
+
function rawToTyped(c) {
|
|
25132
|
+
const sev = c.similarity >= 0.95 ? "high" : c.similarity >= 0.85 ? "medium" : "low";
|
|
25133
|
+
return {
|
|
25134
|
+
observation1: c.existingObservation,
|
|
25135
|
+
observation2: c.newObservation,
|
|
25136
|
+
conflictType: "factual",
|
|
25137
|
+
severity: sev
|
|
25138
|
+
};
|
|
25139
|
+
}
|
|
25140
|
+
var MemoryValidator;
|
|
25141
|
+
var init_MemoryValidator = __esm({
|
|
25142
|
+
"src/agent/MemoryValidator.ts"() {
|
|
25143
|
+
"use strict";
|
|
25144
|
+
init_esm_shims();
|
|
25145
|
+
MemoryValidator = class {
|
|
25146
|
+
contradictionDetector;
|
|
25147
|
+
lowConfidenceThreshold;
|
|
25148
|
+
constructor(contradictionDetector, config = {}) {
|
|
25149
|
+
this.contradictionDetector = contradictionDetector;
|
|
25150
|
+
this.lowConfidenceThreshold = config.lowConfidenceThreshold ?? 0.4;
|
|
25151
|
+
}
|
|
25152
|
+
/**
|
|
25153
|
+
* Check a new observation against an entity's existing knowledge.
|
|
25154
|
+
* Composite: runs the contradiction detector, plus duplicate detection,
|
|
25155
|
+
* plus low-confidence flag if the entity itself looks unreliable.
|
|
25156
|
+
*/
|
|
25157
|
+
async validateConsistency(newObservation, existing) {
|
|
25158
|
+
const issues = [];
|
|
25159
|
+
const suggestions = [];
|
|
25160
|
+
if (existing.observations.includes(newObservation)) {
|
|
25161
|
+
issues.push({
|
|
25162
|
+
kind: "duplicate-observation",
|
|
25163
|
+
message: "Identical observation already present on this entity.",
|
|
25164
|
+
observation: newObservation
|
|
25165
|
+
});
|
|
25166
|
+
suggestions.push("Drop the duplicate; existing observation is unchanged.");
|
|
25167
|
+
}
|
|
25168
|
+
const contradictions = await this.contradictionDetector.detect(existing, [newObservation]);
|
|
25169
|
+
for (const c of contradictions) {
|
|
25170
|
+
issues.push({
|
|
25171
|
+
kind: "semantic-contradiction",
|
|
25172
|
+
message: `New observation semantically contradicts existing observation (similarity ${c.similarity.toFixed(2)}).`,
|
|
25173
|
+
observation: newObservation
|
|
25174
|
+
});
|
|
25175
|
+
suggestions.push(
|
|
25176
|
+
"Either supersede the existing observation (use ContradictionDetector.supersede) or reject the new one."
|
|
25177
|
+
);
|
|
25178
|
+
}
|
|
25179
|
+
const reliability = this.calculateReliability(existing);
|
|
25180
|
+
if (reliability < this.lowConfidenceThreshold) {
|
|
25181
|
+
issues.push({
|
|
25182
|
+
kind: "low-confidence",
|
|
25183
|
+
message: `Entity reliability ${reliability.toFixed(2)} is below threshold ${this.lowConfidenceThreshold}.`
|
|
25184
|
+
});
|
|
25185
|
+
}
|
|
25186
|
+
return {
|
|
25187
|
+
isValid: issues.filter((i) => i.kind !== "low-confidence").length === 0,
|
|
25188
|
+
confidence: 1 - 0.2 * issues.length,
|
|
25189
|
+
// rough confidence drop per issue
|
|
25190
|
+
issues,
|
|
25191
|
+
suggestions
|
|
25192
|
+
};
|
|
25193
|
+
}
|
|
25194
|
+
/**
|
|
25195
|
+
* Detect contradictions within an entity's own observation set.
|
|
25196
|
+
* Delegates to `ContradictionDetector.detect` against a synthetic
|
|
25197
|
+
* "new observations" set (every observation paired against the rest).
|
|
25198
|
+
*
|
|
25199
|
+
* Per ROADMAP §3B.1 spec, returns the extended Contradiction shape
|
|
25200
|
+
* (with `conflictType` + `severity`) — these are derived heuristically
|
|
25201
|
+
* from similarity score because the underlying detector is similarity-
|
|
25202
|
+
* only.
|
|
25203
|
+
*/
|
|
25204
|
+
async detectContradictions(entity) {
|
|
25205
|
+
if (entity.observations.length < 2) return [];
|
|
25206
|
+
const raw = await this.contradictionDetector.detect(entity, entity.observations);
|
|
25207
|
+
const out = [];
|
|
25208
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25209
|
+
for (const c of raw) {
|
|
25210
|
+
const key = [c.existingObservation, c.newObservation].sort().join("||");
|
|
25211
|
+
if (seen.has(key)) continue;
|
|
25212
|
+
seen.add(key);
|
|
25213
|
+
out.push(rawToTyped(c));
|
|
25214
|
+
}
|
|
25215
|
+
return out;
|
|
25216
|
+
}
|
|
25217
|
+
/**
|
|
25218
|
+
* Apply feedback to repair an entity by appending a corrective
|
|
25219
|
+
* observation prefixed with `[repair]`. Returns the repaired entity
|
|
25220
|
+
* — does NOT persist. Caller decides via
|
|
25221
|
+
* `EntityManager.updateEntity` or supersede semantics.
|
|
25222
|
+
*
|
|
25223
|
+
* For full `ConflictResolver`-driven repair against a competing
|
|
25224
|
+
* memory, see `repairWithResolver`.
|
|
25225
|
+
*/
|
|
25226
|
+
async repairMemory(entity, feedback) {
|
|
25227
|
+
return {
|
|
25228
|
+
...entity,
|
|
25229
|
+
observations: [...entity.observations, `[repair] ${feedback}`],
|
|
25230
|
+
lastModified: (/* @__PURE__ */ new Date()).toISOString()
|
|
25231
|
+
};
|
|
25232
|
+
}
|
|
25233
|
+
/**
|
|
25234
|
+
* Repair an entity by delegating to a `ConflictResolver`. Constructs
|
|
25235
|
+
* the minimal `ConflictInfo` from a `Contradiction` finding so callers
|
|
25236
|
+
* don't have to hand-build it. Closes the loop spec'd in ROADMAP §3B.1
|
|
25237
|
+
* for `repairMemory` integration.
|
|
25238
|
+
*
|
|
25239
|
+
* @param entity Primary memory being repaired (must be `AgentEntity`).
|
|
25240
|
+
* @param competing Competing memory to resolve against.
|
|
25241
|
+
* @param contradiction Optional similarity score / context. Severity
|
|
25242
|
+
* is mapped onto `detectionMethod = 'similarity'`.
|
|
25243
|
+
* @param resolver The `ConflictResolver` instance.
|
|
25244
|
+
* @param agents Optional agent-metadata registry (used by the
|
|
25245
|
+
* `trusted_agent` strategy). Empty Map is fine
|
|
25246
|
+
* for the strategies that don't need it.
|
|
25247
|
+
* @returns The resolved memory per the resolver's verdict.
|
|
25248
|
+
*
|
|
25249
|
+
* Throws when neither input is an `AgentEntity` (resolver requires the
|
|
25250
|
+
* extension fields), or when the resolver itself throws (e.g., no
|
|
25251
|
+
* conflicting memories found — should not happen given we provide both).
|
|
25252
|
+
*/
|
|
25253
|
+
async repairWithResolver(entity, competing, resolver, options = {}) {
|
|
25254
|
+
const sim = options.contradiction?.similarity ?? 0.85;
|
|
25255
|
+
const detectionMethod = options.detectionMethod ?? "similarity";
|
|
25256
|
+
const agents = options.agents ?? /* @__PURE__ */ new Map();
|
|
25257
|
+
let suggestedStrategy;
|
|
25258
|
+
if (options.strategy) {
|
|
25259
|
+
suggestedStrategy = options.strategy;
|
|
25260
|
+
} else {
|
|
25261
|
+
const aTs = entity.lastModified ? Date.parse(entity.lastModified) : 0;
|
|
25262
|
+
const bTs = competing.lastModified ? Date.parse(competing.lastModified) : 0;
|
|
25263
|
+
const ageDeltaSeconds = Math.abs(aTs - bTs) / 1e3;
|
|
25264
|
+
suggestedStrategy = ageDeltaSeconds > 60 * 60 * 24 ? "most_recent" : "highest_confidence";
|
|
25265
|
+
}
|
|
25266
|
+
const result = resolver.resolveConflict(
|
|
25267
|
+
{
|
|
25268
|
+
primaryMemory: entity.name,
|
|
25269
|
+
conflictingMemories: [competing.name],
|
|
25270
|
+
detectionMethod,
|
|
25271
|
+
similarityScore: sim,
|
|
25272
|
+
suggestedStrategy,
|
|
25273
|
+
detectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
25274
|
+
},
|
|
25275
|
+
[entity, competing],
|
|
25276
|
+
agents
|
|
25277
|
+
);
|
|
25278
|
+
return result.resolvedMemory;
|
|
25279
|
+
}
|
|
25280
|
+
/**
|
|
25281
|
+
* Validate temporal consistency of observations carrying ISO-8601
|
|
25282
|
+
* timestamps. Looks for either explicit `[T=ISO]` prefixes or
|
|
25283
|
+
* `createdAt`-style metadata. Returns `isValid: false` when any
|
|
25284
|
+
* adjacent pair is out of order.
|
|
25285
|
+
*
|
|
25286
|
+
* Synchronous (no I/O); the spec method is sync.
|
|
25287
|
+
*/
|
|
25288
|
+
validateTemporalOrder(observations) {
|
|
25289
|
+
const issues = [];
|
|
25290
|
+
const stamped = [];
|
|
25291
|
+
for (let i = 0; i < observations.length; i += 1) {
|
|
25292
|
+
const m = observations[i].match(/\[T=([^\]]+)\]/);
|
|
25293
|
+
if (m) {
|
|
25294
|
+
const ts = Date.parse(m[1]);
|
|
25295
|
+
if (Number.isFinite(ts)) {
|
|
25296
|
+
stamped.push({ idx: i, ts, obs: observations[i] });
|
|
25297
|
+
}
|
|
25298
|
+
}
|
|
25299
|
+
}
|
|
25300
|
+
for (let i = 1; i < stamped.length; i += 1) {
|
|
25301
|
+
if (stamped[i].ts < stamped[i - 1].ts) {
|
|
25302
|
+
issues.push({
|
|
25303
|
+
kind: "temporal-disorder",
|
|
25304
|
+
message: `Observation at index ${stamped[i].idx} has earlier timestamp than index ${stamped[i - 1].idx}.`,
|
|
25305
|
+
observation: stamped[i].obs
|
|
25306
|
+
});
|
|
25307
|
+
}
|
|
25308
|
+
}
|
|
25309
|
+
return {
|
|
25310
|
+
isValid: issues.length === 0,
|
|
25311
|
+
confidence: stamped.length >= 2 ? 0.9 : 0.5,
|
|
25312
|
+
// less confident with sparse stamps
|
|
25313
|
+
issues,
|
|
25314
|
+
suggestions: issues.length > 0 ? ["Re-sort observations by parsed timestamp."] : []
|
|
25315
|
+
};
|
|
25316
|
+
}
|
|
25317
|
+
/**
|
|
25318
|
+
* Reliability score in `[0, 1]`. Composite of:
|
|
25319
|
+
* - the entity's own `confidence` field (default 0.5 when absent),
|
|
25320
|
+
* - confirmation count (asymptotic — diminishing returns past 5),
|
|
25321
|
+
* - inverse decay from creation time (older = slightly less reliable
|
|
25322
|
+
* absent reinforcement; very gentle, doesn't dominate).
|
|
25323
|
+
*
|
|
25324
|
+
* Read-only; does not persist anything to the entity.
|
|
25325
|
+
*/
|
|
25326
|
+
calculateReliability(entity) {
|
|
25327
|
+
const ag = entity;
|
|
25328
|
+
const conf = ag.confidence ?? 0.5;
|
|
25329
|
+
const confirmations = ag.confirmationCount ?? 0;
|
|
25330
|
+
const confFactor = confirmations === 0 ? 0 : 1 - 1 / (1 + confirmations / 5);
|
|
25331
|
+
const created = entity.createdAt ? Date.parse(entity.createdAt) : Date.now();
|
|
25332
|
+
const ageDays = Math.max(0, (Date.now() - created) / (1e3 * 60 * 60 * 24));
|
|
25333
|
+
const agePenalty = Math.min(0.3, ageDays / 300);
|
|
25334
|
+
return Math.max(0, Math.min(1, conf * 0.6 + confFactor * 0.3 - agePenalty * 0.1));
|
|
25335
|
+
}
|
|
25336
|
+
};
|
|
25337
|
+
}
|
|
25338
|
+
});
|
|
25339
|
+
|
|
25340
|
+
// src/agent/TrajectoryCompressor.ts
|
|
25341
|
+
function tokenize4(text) {
|
|
25342
|
+
return new Set(
|
|
25343
|
+
text.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((t) => t.length > 0)
|
|
25344
|
+
);
|
|
25345
|
+
}
|
|
25346
|
+
function jaccard(a, b) {
|
|
25347
|
+
if (a.size === 0 && b.size === 0) return 0;
|
|
25348
|
+
let inter = 0;
|
|
25349
|
+
for (const t of a) if (b.has(t)) inter += 1;
|
|
25350
|
+
return inter / (a.size + b.size - inter);
|
|
25351
|
+
}
|
|
25352
|
+
function pickTop3(observations) {
|
|
25353
|
+
if (observations.length <= 3) return observations;
|
|
25354
|
+
const tokens = observations.map(tokenize4);
|
|
25355
|
+
const scores = tokens.map((set, i) => {
|
|
25356
|
+
let overlap = 0;
|
|
25357
|
+
for (let j = 0; j < tokens.length; j += 1) {
|
|
25358
|
+
if (i !== j) {
|
|
25359
|
+
for (const t of set) if (tokens[j].has(t)) overlap += 1;
|
|
25360
|
+
}
|
|
25361
|
+
}
|
|
25362
|
+
return { idx: i, score: overlap };
|
|
25363
|
+
});
|
|
25364
|
+
scores.sort((a, b) => b.score - a.score);
|
|
25365
|
+
return scores.slice(0, 3).map((s) => observations[s.idx]);
|
|
25366
|
+
}
|
|
25367
|
+
var TrajectoryCompressor;
|
|
25368
|
+
var init_TrajectoryCompressor = __esm({
|
|
25369
|
+
"src/agent/TrajectoryCompressor.ts"() {
|
|
25370
|
+
"use strict";
|
|
25371
|
+
init_esm_shims();
|
|
25372
|
+
TrajectoryCompressor = class {
|
|
25373
|
+
contextWindow;
|
|
25374
|
+
redundancyThreshold;
|
|
25375
|
+
constructor(contextWindow, config = {}) {
|
|
25376
|
+
this.contextWindow = contextWindow;
|
|
25377
|
+
this.redundancyThreshold = config.redundancyThreshold ?? 0.7;
|
|
25378
|
+
}
|
|
25379
|
+
/**
|
|
25380
|
+
* Compress an observation sequence into a CompressedMemory.
|
|
25381
|
+
* Strategy: keep observations whose tokens overlap heavily with the
|
|
25382
|
+
* majority (these are the "core" facts), drop low-overlap outliers.
|
|
25383
|
+
* Length-truncate the summary at `maxLength`. No LLM dependency yet —
|
|
25384
|
+
* pluggable later via a summarizer config option.
|
|
25385
|
+
*/
|
|
25386
|
+
async distill(observations, options = {}) {
|
|
25387
|
+
const preserveTemporalOrder = options.preserveTemporalOrder ?? true;
|
|
25388
|
+
const maxLength2 = options.maxLength ?? 2e3;
|
|
25389
|
+
void options.preserveEntities;
|
|
25390
|
+
const originalCount = observations.length;
|
|
25391
|
+
if (originalCount === 0) {
|
|
25392
|
+
return {
|
|
25393
|
+
summary: "",
|
|
25394
|
+
keyFacts: [],
|
|
25395
|
+
originalCount: 0,
|
|
25396
|
+
compressionRatio: 0,
|
|
25397
|
+
preservedDetails: [],
|
|
25398
|
+
discardedDetails: []
|
|
25399
|
+
};
|
|
25400
|
+
}
|
|
25401
|
+
const tokenSets = observations.map((o) => tokenize4(o));
|
|
25402
|
+
const scores = tokenSets.map((set, i) => {
|
|
25403
|
+
let overlap = 0;
|
|
25404
|
+
for (let j = 0; j < tokenSets.length; j += 1) {
|
|
25405
|
+
if (i === j) continue;
|
|
25406
|
+
for (const t of set) if (tokenSets[j].has(t)) overlap += 1;
|
|
25407
|
+
}
|
|
25408
|
+
return { idx: i, score: overlap, obs: observations[i] };
|
|
25409
|
+
});
|
|
25410
|
+
const threshold = options.importanceThreshold ?? 0;
|
|
25411
|
+
const kept = scores.filter((s) => s.score >= threshold);
|
|
25412
|
+
const dropped = scores.filter((s) => s.score < threshold);
|
|
25413
|
+
if (preserveTemporalOrder) kept.sort((a, b) => a.idx - b.idx);
|
|
25414
|
+
else kept.sort((a, b) => b.score - a.score);
|
|
25415
|
+
const keyFacts = kept.slice(0, Math.min(kept.length, 10)).map((s) => s.obs);
|
|
25416
|
+
let summary = keyFacts.join(" ");
|
|
25417
|
+
if (summary.length > maxLength2) summary = summary.slice(0, maxLength2).trimEnd() + "\u2026";
|
|
25418
|
+
const totalLength = observations.join(" ").length || 1;
|
|
25419
|
+
return {
|
|
25420
|
+
summary,
|
|
25421
|
+
keyFacts,
|
|
25422
|
+
originalCount,
|
|
25423
|
+
compressionRatio: summary.length / totalLength,
|
|
25424
|
+
preservedDetails: kept.map((s) => s.obs),
|
|
25425
|
+
discardedDetails: dropped.map((s) => s.obs)
|
|
25426
|
+
};
|
|
25427
|
+
}
|
|
25428
|
+
/**
|
|
25429
|
+
* Produce a coarsened view of a set of entities at one of three
|
|
25430
|
+
* granularities. `fine` returns the entities unchanged; `medium`
|
|
25431
|
+
* trims observations to the top-3 most overlap-y per entity;
|
|
25432
|
+
* `coarse` distills each entity's observations into a single summary.
|
|
25433
|
+
*/
|
|
25434
|
+
async abstractAtLevel(memories, granularity) {
|
|
25435
|
+
if (granularity === "fine") return memories;
|
|
25436
|
+
const out = [];
|
|
25437
|
+
for (const e of memories) {
|
|
25438
|
+
if (granularity === "medium") {
|
|
25439
|
+
const top3 = pickTop3(e.observations);
|
|
25440
|
+
out.push({ ...e, observations: top3 });
|
|
25441
|
+
} else {
|
|
25442
|
+
const distilled = await this.distill(e.observations, { maxLength: 200 });
|
|
25443
|
+
out.push({ ...e, observations: [distilled.summary] });
|
|
25444
|
+
}
|
|
25445
|
+
}
|
|
25446
|
+
return out;
|
|
25447
|
+
}
|
|
25448
|
+
/**
|
|
25449
|
+
* Compress a working-memory text blob to fit within `maxTokens`.
|
|
25450
|
+
* Delegates to `ContextWindowManager.compressForContext` and chooses
|
|
25451
|
+
* the compression level based on how aggressively we need to shrink.
|
|
25452
|
+
* The `working` parameter is intentionally typed `string` here — the
|
|
25453
|
+
* spec talks about `WorkingMemory` but in practice the compressor
|
|
25454
|
+
* operates on serialized text.
|
|
25455
|
+
*/
|
|
25456
|
+
async foldContext(working, maxTokens) {
|
|
25457
|
+
const estTokens = Math.ceil(working.length / 4);
|
|
25458
|
+
if (estTokens <= maxTokens) return working;
|
|
25459
|
+
const ratio = estTokens / maxTokens;
|
|
25460
|
+
const level = ratio > 2 ? "aggressive" : ratio > 1.3 ? "medium" : "light";
|
|
25461
|
+
const result = this.contextWindow.compressForContext(working, { level });
|
|
25462
|
+
return result.compressed;
|
|
25463
|
+
}
|
|
25464
|
+
/**
|
|
25465
|
+
* Identify groups of entities whose observation sets are largely
|
|
25466
|
+
* duplicates. Pairs entities whose union-of-observations Jaccard
|
|
25467
|
+
* exceeds the configured threshold. O(n²) pairwise; suitable for
|
|
25468
|
+
* graphs up to ~1k entities — beyond that, a candidate-blocking
|
|
25469
|
+
* pass on tags/projectId would be the natural extension.
|
|
25470
|
+
*
|
|
25471
|
+
* Algorithmic caveat (greedy single-link): an entity is absorbed
|
|
25472
|
+
* into the FIRST seed it overlaps with above threshold; results
|
|
25473
|
+
* therefore depend on input ordering when an entity would qualify
|
|
25474
|
+
* for multiple seeds. For correctness-critical clustering, use
|
|
25475
|
+
* union-find or complete-link clustering instead.
|
|
25476
|
+
*/
|
|
25477
|
+
async findRedundancies(entities) {
|
|
25478
|
+
const groups = [];
|
|
25479
|
+
const visited = /* @__PURE__ */ new Set();
|
|
25480
|
+
for (let i = 0; i < entities.length; i += 1) {
|
|
25481
|
+
if (visited.has(entities[i].name)) continue;
|
|
25482
|
+
const cluster = [entities[i]];
|
|
25483
|
+
const sims = [];
|
|
25484
|
+
visited.add(entities[i].name);
|
|
25485
|
+
for (let j = i + 1; j < entities.length; j += 1) {
|
|
25486
|
+
if (visited.has(entities[j].name)) continue;
|
|
25487
|
+
const sim = jaccard(
|
|
25488
|
+
new Set(entities[i].observations.flatMap((o) => Array.from(tokenize4(o)))),
|
|
25489
|
+
new Set(entities[j].observations.flatMap((o) => Array.from(tokenize4(o))))
|
|
25490
|
+
);
|
|
25491
|
+
if (sim >= this.redundancyThreshold) {
|
|
25492
|
+
cluster.push(entities[j]);
|
|
25493
|
+
sims.push(sim);
|
|
25494
|
+
visited.add(entities[j].name);
|
|
25495
|
+
}
|
|
25496
|
+
}
|
|
25497
|
+
if (cluster.length > 1) {
|
|
25498
|
+
const avg = sims.reduce((a, b) => a + b, 0) / sims.length;
|
|
25499
|
+
groups.push({
|
|
25500
|
+
entities: cluster,
|
|
25501
|
+
canonicalName: cluster[0].name,
|
|
25502
|
+
avgSimilarity: avg
|
|
25503
|
+
});
|
|
25504
|
+
}
|
|
25505
|
+
}
|
|
25506
|
+
return groups;
|
|
25507
|
+
}
|
|
25508
|
+
/**
|
|
25509
|
+
* Collapse a redundancy group into a single canonical entity per
|
|
25510
|
+
* strategy. Doesn't persist — caller is responsible for the actual
|
|
25511
|
+
* `EntityManager.deleteEntities(...)` + `createEntity(merged)` dance
|
|
25512
|
+
* if they want the change durable.
|
|
25513
|
+
*/
|
|
25514
|
+
async mergeRedundant(group, strategy) {
|
|
25515
|
+
if (group.entities.length === 0) {
|
|
25516
|
+
throw new Error("TrajectoryCompressor.mergeRedundant: empty group");
|
|
25517
|
+
}
|
|
25518
|
+
let canonical;
|
|
25519
|
+
switch (strategy) {
|
|
25520
|
+
case "keep-newest":
|
|
25521
|
+
canonical = group.entities.reduce((acc, e) => {
|
|
25522
|
+
const epoch = "1970-01-01T00:00:00Z";
|
|
25523
|
+
const accT = Date.parse(acc.lastModified ?? acc.createdAt ?? epoch);
|
|
25524
|
+
const eT = Date.parse(e.lastModified ?? e.createdAt ?? epoch);
|
|
25525
|
+
return Number.isFinite(eT) && eT > (Number.isFinite(accT) ? accT : 0) ? e : acc;
|
|
25526
|
+
});
|
|
25527
|
+
break;
|
|
25528
|
+
case "keep-most-confident":
|
|
25529
|
+
canonical = group.entities.reduce((acc, e) => {
|
|
25530
|
+
const ac = acc.confidence ?? 0;
|
|
25531
|
+
const ec = e.confidence ?? 0;
|
|
25532
|
+
return ec > ac ? e : acc;
|
|
25533
|
+
});
|
|
25534
|
+
break;
|
|
25535
|
+
case "union-observations": {
|
|
25536
|
+
const head = group.entities[0];
|
|
25537
|
+
const allObs = /* @__PURE__ */ new Set();
|
|
25538
|
+
for (const e of group.entities) for (const o of e.observations) allObs.add(o);
|
|
25539
|
+
canonical = { ...head, observations: Array.from(allObs) };
|
|
25540
|
+
break;
|
|
25541
|
+
}
|
|
25542
|
+
}
|
|
25543
|
+
return canonical;
|
|
25544
|
+
}
|
|
25545
|
+
};
|
|
25546
|
+
}
|
|
25547
|
+
});
|
|
25548
|
+
|
|
25549
|
+
// src/agent/ExperienceExtractor.ts
|
|
25550
|
+
function tokenize5(text) {
|
|
25551
|
+
return new Set(
|
|
25552
|
+
text.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((t) => t.length > 2)
|
|
25553
|
+
);
|
|
25554
|
+
}
|
|
25555
|
+
function countTokens(texts) {
|
|
25556
|
+
const out = /* @__PURE__ */ new Map();
|
|
25557
|
+
for (const text of texts) {
|
|
25558
|
+
for (const tok of tokenize5(text)) {
|
|
25559
|
+
out.set(tok, (out.get(tok) ?? 0) + 1);
|
|
25560
|
+
}
|
|
25561
|
+
}
|
|
25562
|
+
return out;
|
|
25563
|
+
}
|
|
25564
|
+
function jaccard2(a, b) {
|
|
25565
|
+
if (a.size === 0 && b.size === 0) return 0;
|
|
25566
|
+
let inter = 0;
|
|
25567
|
+
for (const t of a) if (b.has(t)) inter += 1;
|
|
25568
|
+
return inter / (a.size + b.size - inter);
|
|
25569
|
+
}
|
|
25570
|
+
function trajectoryTokens(t, method) {
|
|
25571
|
+
if (method === "structural") {
|
|
25572
|
+
return new Set(t.actions.map((a) => a.name));
|
|
25573
|
+
}
|
|
25574
|
+
const all2 = /* @__PURE__ */ new Set();
|
|
25575
|
+
for (const o of t.observations) for (const tok of tokenize5(o)) all2.add(tok);
|
|
25576
|
+
return all2;
|
|
25577
|
+
}
|
|
25578
|
+
function extractField(t, field) {
|
|
25579
|
+
if (field === "outcome") return t.outcome;
|
|
25580
|
+
return t.context[field];
|
|
25581
|
+
}
|
|
25582
|
+
var ExperienceExtractor;
|
|
25583
|
+
var init_ExperienceExtractor = __esm({
|
|
25584
|
+
"src/agent/ExperienceExtractor.ts"() {
|
|
25585
|
+
"use strict";
|
|
25586
|
+
init_esm_shims();
|
|
25587
|
+
ExperienceExtractor = class {
|
|
25588
|
+
patternDetector;
|
|
25589
|
+
minPatternOccurrences;
|
|
25590
|
+
similarityThreshold;
|
|
25591
|
+
constructor(patternDetector, config = {}) {
|
|
25592
|
+
this.patternDetector = patternDetector;
|
|
25593
|
+
this.minPatternOccurrences = config.minPatternOccurrences ?? 2;
|
|
25594
|
+
this.similarityThreshold = config.similarityThreshold ?? 0.6;
|
|
25595
|
+
}
|
|
25596
|
+
/**
|
|
25597
|
+
* Derive rules from contrastive pairs. Strategy: tokens appearing
|
|
25598
|
+
* disproportionately in successes (vs. failures) become condition
|
|
25599
|
+
* antecedents; the next action after the distinguishing token
|
|
25600
|
+
* becomes the rule's recommended action.
|
|
25601
|
+
*
|
|
25602
|
+
* Lightweight — no embeddings or LLMs. Suitable for the "what does
|
|
25603
|
+
* the agent do differently when it succeeds" question at scale.
|
|
25604
|
+
*/
|
|
25605
|
+
async extractFromContrastivePairs(success, failure) {
|
|
25606
|
+
if (success.length === 0 || failure.length === 0) return [];
|
|
25607
|
+
const successTokens = countTokens(success.flatMap((t) => t.observations));
|
|
25608
|
+
const failureTokens = countTokens(failure.flatMap((t) => t.observations));
|
|
25609
|
+
const rules = [];
|
|
25610
|
+
for (const [tok, sCount] of successTokens.entries()) {
|
|
25611
|
+
const fCount = failureTokens.get(tok) ?? 0;
|
|
25612
|
+
if (sCount >= 2 && sCount >= 2 * fCount) {
|
|
25613
|
+
const actionCounts = /* @__PURE__ */ new Map();
|
|
25614
|
+
for (const t of success) {
|
|
25615
|
+
if (t.observations.some((o) => o.toLowerCase().includes(tok))) {
|
|
25616
|
+
for (const a of t.actions) {
|
|
25617
|
+
actionCounts.set(a.name, (actionCounts.get(a.name) ?? 0) + 1);
|
|
25618
|
+
}
|
|
25619
|
+
}
|
|
25620
|
+
}
|
|
25621
|
+
const topAction = Array.from(actionCounts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown";
|
|
25622
|
+
rules.push({
|
|
25623
|
+
condition: `observation contains "${tok}"`,
|
|
25624
|
+
action: topAction,
|
|
25625
|
+
confidence: sCount / (sCount + fCount + 1),
|
|
25626
|
+
supportCount: sCount,
|
|
25627
|
+
contraCount: fCount
|
|
25628
|
+
});
|
|
25629
|
+
}
|
|
25630
|
+
}
|
|
25631
|
+
return rules.sort((a, b) => b.confidence - a.confidence).slice(0, 10);
|
|
25632
|
+
}
|
|
25633
|
+
/**
|
|
25634
|
+
* Abstract a pattern across trajectories' observations. Delegates
|
|
25635
|
+
* to `PatternDetector.detectPatterns` and lifts the result onto
|
|
25636
|
+
* the spec's `HeuristicGuideline` shape with trajectory provenance.
|
|
25637
|
+
*
|
|
25638
|
+
* `similarityThreshold` is currently unused by the underlying
|
|
25639
|
+
* `detectPatterns` (it operates on token-template equality, not
|
|
25640
|
+
* similarity); kept in the signature for spec compliance and future
|
|
25641
|
+
* use when an embedding-based variant lands.
|
|
25642
|
+
*/
|
|
25643
|
+
async abstractPattern(trajectories, similarityThreshold) {
|
|
25644
|
+
void similarityThreshold;
|
|
25645
|
+
const allObs = trajectories.flatMap((t) => t.observations);
|
|
25646
|
+
const patterns = this.patternDetector.detectPatterns(
|
|
25647
|
+
allObs,
|
|
25648
|
+
this.minPatternOccurrences
|
|
25649
|
+
);
|
|
25650
|
+
if (patterns.length === 0) {
|
|
25651
|
+
return { pattern: "", variables: [], occurrences: 0, sourceTrajectoryIds: [] };
|
|
25652
|
+
}
|
|
25653
|
+
const top = patterns.sort((a, b) => b.occurrences - a.occurrences)[0];
|
|
25654
|
+
const sourceIds = /* @__PURE__ */ new Set();
|
|
25655
|
+
const variableValues = new Set(top.variables);
|
|
25656
|
+
for (const t of trajectories) {
|
|
25657
|
+
for (const o of t.observations) {
|
|
25658
|
+
if (Array.from(variableValues).some((v) => o.includes(v))) {
|
|
25659
|
+
sourceIds.add(t.id);
|
|
25660
|
+
break;
|
|
25661
|
+
}
|
|
25662
|
+
}
|
|
25663
|
+
}
|
|
25664
|
+
return {
|
|
25665
|
+
pattern: top.pattern,
|
|
25666
|
+
variables: top.variables ?? [],
|
|
25667
|
+
occurrences: top.occurrences,
|
|
25668
|
+
sourceTrajectoryIds: Array.from(sourceIds)
|
|
25669
|
+
};
|
|
25670
|
+
}
|
|
25671
|
+
/**
|
|
25672
|
+
* Learn the decision boundary for a binary outcome split. Currently
|
|
25673
|
+
* supports `outcome` field (success vs. failure); other field names
|
|
25674
|
+
* fall back to the `Outcome` lookup. Returns the most-distinguishing
|
|
25675
|
+
* tokens per side.
|
|
25676
|
+
*/
|
|
25677
|
+
async learnDecisionBoundary(trajectories, outcomeField) {
|
|
25678
|
+
const positive = trajectories.filter(
|
|
25679
|
+
(t) => extractField(t, outcomeField) === "success"
|
|
25680
|
+
);
|
|
25681
|
+
const negative = trajectories.filter(
|
|
25682
|
+
(t) => extractField(t, outcomeField) === "failure"
|
|
25683
|
+
);
|
|
25684
|
+
const posTokens = countTokens(positive.flatMap((t) => t.observations));
|
|
25685
|
+
const negTokens = countTokens(negative.flatMap((t) => t.observations));
|
|
25686
|
+
const presence = [];
|
|
25687
|
+
const absence = [];
|
|
25688
|
+
for (const [tok, p] of posTokens) {
|
|
25689
|
+
const n = negTokens.get(tok) ?? 0;
|
|
25690
|
+
if (p >= 2 && p >= 2 * n) presence.push(tok);
|
|
25691
|
+
}
|
|
25692
|
+
for (const [tok, n] of negTokens) {
|
|
25693
|
+
const p = posTokens.get(tok) ?? 0;
|
|
25694
|
+
if (n >= 2 && n >= 2 * p) absence.push(tok);
|
|
25695
|
+
}
|
|
25696
|
+
const total = positive.length + negative.length;
|
|
25697
|
+
return {
|
|
25698
|
+
presenceTokens: presence.slice(0, 10),
|
|
25699
|
+
absenceTokens: absence.slice(0, 10),
|
|
25700
|
+
outcomeIfPresent: "success",
|
|
25701
|
+
outcomeIfAbsent: "failure",
|
|
25702
|
+
confidence: total === 0 ? 0 : Math.min(positive.length, negative.length) / total
|
|
25703
|
+
};
|
|
25704
|
+
}
|
|
25705
|
+
/**
|
|
25706
|
+
* Cluster trajectories by the chosen method. Lightweight: no
|
|
25707
|
+
* embeddings — `semantic` and `structural` both use token-Jaccard
|
|
25708
|
+
* with different normalization; `outcome` simply groups by the
|
|
25709
|
+
* `Outcome` value.
|
|
25710
|
+
*
|
|
25711
|
+
* Algorithmic caveat (greedy single-link for semantic/structural):
|
|
25712
|
+
* a trajectory is absorbed into the FIRST seed it overlaps with
|
|
25713
|
+
* above the configured similarity threshold, regardless of whether
|
|
25714
|
+
* a later seed would match more strongly. Results therefore depend
|
|
25715
|
+
* on input ordering, and "chain" clusters (A↔B, B↔C, but A↔C far
|
|
25716
|
+
* apart) can form under low thresholds. The `cohesion` field on
|
|
25717
|
+
* each `TrajectoryCluster` surfaces this — downstream
|
|
25718
|
+
* `synthesizeExperience` already passes cohesion through to
|
|
25719
|
+
* `Experience.confidence`. For higher-quality clustering, a
|
|
25720
|
+
* complete-link or union-find variant would be the natural
|
|
25721
|
+
* extension.
|
|
25722
|
+
*/
|
|
25723
|
+
async clusterTrajectories(trajectories, method) {
|
|
25724
|
+
if (trajectories.length === 0) return [];
|
|
25725
|
+
if (method === "outcome") {
|
|
25726
|
+
const groups = /* @__PURE__ */ new Map();
|
|
25727
|
+
for (const t of trajectories) {
|
|
25728
|
+
const arr = groups.get(t.outcome) ?? [];
|
|
25729
|
+
arr.push(t);
|
|
25730
|
+
groups.set(t.outcome, arr);
|
|
25731
|
+
}
|
|
25732
|
+
return Array.from(groups.entries()).map(([outcome, ts], i) => ({
|
|
25733
|
+
id: `cluster-outcome-${i}-${outcome}`,
|
|
25734
|
+
method: "outcome",
|
|
25735
|
+
trajectories: ts,
|
|
25736
|
+
cohesion: 1
|
|
25737
|
+
// outcome equality = perfect cohesion by definition
|
|
25738
|
+
}));
|
|
25739
|
+
}
|
|
25740
|
+
const clusters = [];
|
|
25741
|
+
const sims = [];
|
|
25742
|
+
const visited = /* @__PURE__ */ new Set();
|
|
25743
|
+
for (const t of trajectories) {
|
|
25744
|
+
if (visited.has(t.id)) continue;
|
|
25745
|
+
const cluster = [t];
|
|
25746
|
+
const cSims = [];
|
|
25747
|
+
visited.add(t.id);
|
|
25748
|
+
const tTokens = trajectoryTokens(t, method);
|
|
25749
|
+
for (const u of trajectories) {
|
|
25750
|
+
if (visited.has(u.id)) continue;
|
|
25751
|
+
const sim = jaccard2(tTokens, trajectoryTokens(u, method));
|
|
25752
|
+
if (sim >= this.similarityThreshold) {
|
|
25753
|
+
cluster.push(u);
|
|
25754
|
+
cSims.push(sim);
|
|
25755
|
+
visited.add(u.id);
|
|
25756
|
+
}
|
|
25757
|
+
}
|
|
25758
|
+
clusters.push(cluster);
|
|
25759
|
+
sims.push(cSims);
|
|
25760
|
+
}
|
|
25761
|
+
return clusters.map((cluster, i) => ({
|
|
25762
|
+
id: `cluster-${method}-${i}`,
|
|
25763
|
+
method,
|
|
25764
|
+
trajectories: cluster,
|
|
25765
|
+
cohesion: sims[i].length === 0 ? 1 : sims[i].reduce((a, b) => a + b, 0) / sims[i].length
|
|
25766
|
+
}));
|
|
25767
|
+
}
|
|
25768
|
+
/**
|
|
25769
|
+
* Synthesize a transferable `Experience` from a cluster. Picks the
|
|
25770
|
+
* `type` heuristically (procedure if cluster is action-heavy;
|
|
25771
|
+
* heuristic otherwise) and uses the most-common-pattern across the
|
|
25772
|
+
* cluster as the experience content.
|
|
25773
|
+
*/
|
|
25774
|
+
async synthesizeExperience(cluster) {
|
|
25775
|
+
const id = `exp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
25776
|
+
const obs = cluster.trajectories.flatMap((t) => t.observations);
|
|
25777
|
+
const actionCount = cluster.trajectories.reduce((acc, t) => acc + t.actions.length, 0);
|
|
25778
|
+
const type = actionCount > cluster.trajectories.length * 2 ? "procedure" : "heuristic";
|
|
25779
|
+
const patterns = this.patternDetector.detectPatterns(obs, 2);
|
|
25780
|
+
const content = patterns[0]?.pattern ?? obs[0] ?? "";
|
|
25781
|
+
const counts = countTokens(obs);
|
|
25782
|
+
const applicability = Array.from(counts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t]) => t);
|
|
25783
|
+
return {
|
|
25784
|
+
id,
|
|
25785
|
+
type,
|
|
25786
|
+
content,
|
|
25787
|
+
applicability,
|
|
25788
|
+
confidence: cluster.cohesion,
|
|
25789
|
+
sourceTrajectories: cluster.trajectories.map((t) => t.id),
|
|
25790
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
25791
|
+
};
|
|
25792
|
+
}
|
|
25793
|
+
};
|
|
25794
|
+
}
|
|
25795
|
+
});
|
|
25796
|
+
|
|
25797
|
+
// src/agent/procedural/ProcedureStore.ts
|
|
25798
|
+
function encodeObservations(procedure) {
|
|
25799
|
+
const observations = [];
|
|
25800
|
+
if (procedure.description && procedure.description.trim() !== "") {
|
|
25801
|
+
observations.push(procedure.description);
|
|
25802
|
+
}
|
|
25803
|
+
observations.push(`${STEPS_PREFIX}${JSON.stringify(procedure.steps)}`);
|
|
25804
|
+
observations.push(
|
|
25805
|
+
`${META_PREFIX}${JSON.stringify({
|
|
25806
|
+
triggers: procedure.triggers ?? [],
|
|
25807
|
+
successRate: procedure.successRate ?? 0,
|
|
25808
|
+
executionCount: procedure.executionCount ?? 0
|
|
25809
|
+
})}`
|
|
25810
|
+
);
|
|
25811
|
+
return observations;
|
|
25812
|
+
}
|
|
25813
|
+
function decodeProcedure(id, observations) {
|
|
25814
|
+
let steps = [];
|
|
25815
|
+
let triggers = [];
|
|
25816
|
+
let successRate = 0;
|
|
25817
|
+
let executionCount = 0;
|
|
25818
|
+
const descriptionLines = [];
|
|
25819
|
+
for (const obs of observations) {
|
|
25820
|
+
if (obs.startsWith(STEPS_PREFIX)) {
|
|
25821
|
+
try {
|
|
25822
|
+
const parsed = JSON.parse(obs.slice(STEPS_PREFIX.length));
|
|
25823
|
+
if (Array.isArray(parsed)) steps = parsed;
|
|
25824
|
+
} catch {
|
|
25825
|
+
}
|
|
25826
|
+
} else if (obs.startsWith(META_PREFIX)) {
|
|
25827
|
+
try {
|
|
25828
|
+
const parsed = JSON.parse(obs.slice(META_PREFIX.length));
|
|
25829
|
+
triggers = parsed.triggers ?? [];
|
|
25830
|
+
successRate = parsed.successRate ?? 0;
|
|
25831
|
+
executionCount = parsed.executionCount ?? 0;
|
|
25832
|
+
} catch {
|
|
25833
|
+
}
|
|
25834
|
+
} else {
|
|
25835
|
+
descriptionLines.push(obs);
|
|
25836
|
+
}
|
|
25837
|
+
}
|
|
25838
|
+
return {
|
|
25839
|
+
id,
|
|
25840
|
+
name: id,
|
|
25841
|
+
description: descriptionLines.join("\n"),
|
|
25842
|
+
steps,
|
|
25843
|
+
triggers,
|
|
25844
|
+
successRate,
|
|
25845
|
+
executionCount
|
|
25846
|
+
};
|
|
25847
|
+
}
|
|
25848
|
+
var STEPS_PREFIX, META_PREFIX, PROCEDURE_ENTITY_TYPE, ProcedureStore;
|
|
25849
|
+
var init_ProcedureStore = __esm({
|
|
25850
|
+
"src/agent/procedural/ProcedureStore.ts"() {
|
|
25851
|
+
"use strict";
|
|
25852
|
+
init_esm_shims();
|
|
25853
|
+
STEPS_PREFIX = "[procedure-steps]:";
|
|
25854
|
+
META_PREFIX = "[procedure-meta]:";
|
|
25855
|
+
PROCEDURE_ENTITY_TYPE = "procedure";
|
|
25856
|
+
ProcedureStore = class {
|
|
25857
|
+
constructor(entityManager) {
|
|
25858
|
+
this.entityManager = entityManager;
|
|
25859
|
+
}
|
|
25860
|
+
/**
|
|
25861
|
+
* Persist a new procedure. The entity name = `procedure.id`. Steps and
|
|
25862
|
+
* metadata are encoded as JSON observations alongside any caller-supplied
|
|
25863
|
+
* description observation. Idempotent on duplicate id (relies on
|
|
25864
|
+
* `EntityManager.createEntities` semantics).
|
|
25865
|
+
*/
|
|
25866
|
+
async save(procedure) {
|
|
25867
|
+
const observations = encodeObservations(procedure);
|
|
25868
|
+
await this.entityManager.createEntities([
|
|
25869
|
+
{
|
|
25870
|
+
name: procedure.id,
|
|
25871
|
+
entityType: PROCEDURE_ENTITY_TYPE,
|
|
25872
|
+
observations,
|
|
25873
|
+
tags: ["procedure", ...procedure.triggers ?? []]
|
|
25874
|
+
}
|
|
25875
|
+
]);
|
|
25876
|
+
}
|
|
25877
|
+
/**
|
|
25878
|
+
* Load a procedure by id, or null if not found. Tolerant of partial
|
|
25879
|
+
* encodings — steps default to `[]`, meta to zeroed fields.
|
|
25880
|
+
*/
|
|
25881
|
+
async load(id) {
|
|
25882
|
+
const entity = await this.entityManager.getEntity(id);
|
|
25883
|
+
if (!entity || entity.entityType !== PROCEDURE_ENTITY_TYPE) return null;
|
|
25884
|
+
return decodeProcedure(id, entity.observations);
|
|
25885
|
+
}
|
|
25886
|
+
/**
|
|
25887
|
+
* Replace an existing procedure's steps + metadata. Throws if the
|
|
25888
|
+
* entity doesn't exist or isn't a procedure.
|
|
25889
|
+
*/
|
|
25890
|
+
async update(procedure) {
|
|
25891
|
+
await this.entityManager.updateEntity(procedure.id, {
|
|
25892
|
+
observations: encodeObservations(procedure),
|
|
25893
|
+
tags: ["procedure", ...procedure.triggers ?? []]
|
|
25894
|
+
});
|
|
25895
|
+
}
|
|
25896
|
+
};
|
|
25897
|
+
}
|
|
25898
|
+
});
|
|
25899
|
+
|
|
25900
|
+
// src/agent/procedural/StepSequencer.ts
|
|
25901
|
+
var StepSequencer;
|
|
25902
|
+
var init_StepSequencer = __esm({
|
|
25903
|
+
"src/agent/procedural/StepSequencer.ts"() {
|
|
25904
|
+
"use strict";
|
|
25905
|
+
init_esm_shims();
|
|
25906
|
+
StepSequencer = class {
|
|
25907
|
+
constructor(procedure) {
|
|
25908
|
+
this.procedure = procedure;
|
|
25909
|
+
}
|
|
25910
|
+
cursor = 0;
|
|
25911
|
+
/** When set, all `current()` / `next()` calls return this fallback chain
|
|
25912
|
+
* instead of the main steps until cleared. */
|
|
25913
|
+
activeFallback = null;
|
|
25914
|
+
/** Steps in 1-indexed order. Read-only. */
|
|
25915
|
+
get steps() {
|
|
25916
|
+
return this.procedure.steps;
|
|
25917
|
+
}
|
|
25918
|
+
/** Index of the next step to execute (0-based). Public for tests. */
|
|
25919
|
+
get cursorIndex() {
|
|
25920
|
+
return this.cursor;
|
|
25921
|
+
}
|
|
25922
|
+
/** Whether all main-track steps have been consumed. Fallbacks may still run. */
|
|
25923
|
+
isComplete() {
|
|
25924
|
+
return this.activeFallback === null && this.cursor >= this.procedure.steps.length;
|
|
25925
|
+
}
|
|
25926
|
+
/** Return the step about to execute, or null if exhausted. */
|
|
25927
|
+
current() {
|
|
25928
|
+
if (this.activeFallback) return this.activeFallback;
|
|
25929
|
+
return this.procedure.steps[this.cursor] ?? null;
|
|
25930
|
+
}
|
|
25931
|
+
/**
|
|
25932
|
+
* Advance the cursor and return the new current step (or null when
|
|
25933
|
+
* complete). Clears any active fallback — fallbacks are single-step
|
|
25934
|
+
* by design; deeper branching needs a nested fallback in the step's
|
|
25935
|
+
* `fallback.fallback`.
|
|
25936
|
+
*/
|
|
25937
|
+
next() {
|
|
25938
|
+
if (this.activeFallback) {
|
|
25939
|
+
this.activeFallback = null;
|
|
25940
|
+
this.cursor++;
|
|
25941
|
+
return this.current();
|
|
25942
|
+
}
|
|
25943
|
+
this.cursor++;
|
|
25944
|
+
return this.current();
|
|
25945
|
+
}
|
|
25946
|
+
/**
|
|
25947
|
+
* Switch to the current step's `fallback` chain. The next `current()`
|
|
25948
|
+
* call will return the fallback's first step. Throws if the current
|
|
25949
|
+
* step has no fallback (caller should test before invoking).
|
|
25950
|
+
*/
|
|
25951
|
+
branchToFallback() {
|
|
25952
|
+
const step = this.current();
|
|
25953
|
+
if (!step?.fallback) {
|
|
25954
|
+
throw new Error(`Step ${step?.order ?? "?"} has no fallback`);
|
|
25955
|
+
}
|
|
25956
|
+
this.activeFallback = step.fallback;
|
|
25957
|
+
}
|
|
25958
|
+
/** Reset the cursor and clear any fallback — start over. */
|
|
25959
|
+
reset() {
|
|
25960
|
+
this.cursor = 0;
|
|
25961
|
+
this.activeFallback = null;
|
|
25962
|
+
}
|
|
25963
|
+
};
|
|
25964
|
+
}
|
|
25965
|
+
});
|
|
25966
|
+
|
|
25967
|
+
// src/agent/procedural/ProcedureManager.ts
|
|
25968
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
25969
|
+
function tokenize6(s) {
|
|
25970
|
+
return new Set(
|
|
25971
|
+
s.toLowerCase().split(/[^a-z0-9]+/g).filter((t) => t.length >= 2)
|
|
25972
|
+
);
|
|
25973
|
+
}
|
|
25974
|
+
function clamp01(x) {
|
|
25975
|
+
return Math.max(0, Math.min(1, x));
|
|
25976
|
+
}
|
|
25977
|
+
var ProcedureManager;
|
|
25978
|
+
var init_ProcedureManager = __esm({
|
|
25979
|
+
"src/agent/procedural/ProcedureManager.ts"() {
|
|
25980
|
+
"use strict";
|
|
25981
|
+
init_esm_shims();
|
|
25982
|
+
init_ProcedureStore();
|
|
25983
|
+
init_StepSequencer();
|
|
25984
|
+
ProcedureManager = class {
|
|
25985
|
+
store;
|
|
25986
|
+
successRateAlpha;
|
|
25987
|
+
constructor(entityManager, config = {}) {
|
|
25988
|
+
this.store = new ProcedureStore(entityManager);
|
|
25989
|
+
this.successRateAlpha = config.successRateAlpha ?? 0.2;
|
|
25990
|
+
}
|
|
25991
|
+
/**
|
|
25992
|
+
* Persist a new procedure. Auto-generates `id` (and falls back to it
|
|
25993
|
+
* for `name`) when caller omits them. Throws if the id collides — the
|
|
25994
|
+
* caller should `getProcedure` first if upsert semantics are needed.
|
|
25995
|
+
*/
|
|
25996
|
+
async addProcedure(input) {
|
|
25997
|
+
if (!input.steps) {
|
|
25998
|
+
throw new Error("addProcedure: steps[] is required");
|
|
25999
|
+
}
|
|
26000
|
+
const id = input.id ?? `proc-${randomUUID2()}`;
|
|
26001
|
+
const procedure = {
|
|
26002
|
+
id,
|
|
26003
|
+
name: input.name ?? id,
|
|
26004
|
+
description: input.description ?? "",
|
|
26005
|
+
steps: input.steps,
|
|
26006
|
+
triggers: input.triggers ?? [],
|
|
26007
|
+
successRate: input.successRate ?? 0,
|
|
26008
|
+
executionCount: input.executionCount ?? 0,
|
|
26009
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26010
|
+
lastModified: (/* @__PURE__ */ new Date()).toISOString()
|
|
26011
|
+
};
|
|
26012
|
+
await this.store.save(procedure);
|
|
26013
|
+
return procedure;
|
|
26014
|
+
}
|
|
26015
|
+
/** Load by id, or null. */
|
|
26016
|
+
async getProcedure(id) {
|
|
26017
|
+
return this.store.load(id);
|
|
26018
|
+
}
|
|
26019
|
+
/**
|
|
26020
|
+
* Stateless lookup of a specific step by 1-indexed `stepOrder`. Returns
|
|
26021
|
+
* null when the procedure has no such step.
|
|
26022
|
+
*/
|
|
26023
|
+
async getStep(procedureId, stepOrder) {
|
|
26024
|
+
const proc = await this.getProcedure(procedureId);
|
|
26025
|
+
if (!proc) return null;
|
|
26026
|
+
return proc.steps.find((s) => s.order === stepOrder) ?? null;
|
|
26027
|
+
}
|
|
26028
|
+
/**
|
|
26029
|
+
* Look up the step that follows `currentOrder`. Returns null when at
|
|
26030
|
+
* the end of the procedure or the procedure does not exist.
|
|
26031
|
+
*/
|
|
26032
|
+
async getNextStep(procedureId, currentOrder) {
|
|
26033
|
+
const proc = await this.getProcedure(procedureId);
|
|
26034
|
+
if (!proc) return null;
|
|
26035
|
+
const idx = proc.steps.findIndex((s) => s.order === currentOrder);
|
|
26036
|
+
if (idx < 0 || idx + 1 >= proc.steps.length) return null;
|
|
26037
|
+
return proc.steps[idx + 1];
|
|
26038
|
+
}
|
|
26039
|
+
/**
|
|
26040
|
+
* Open a fresh `StepSequencer` for the named procedure. Returns null
|
|
26041
|
+
* when the procedure doesn't exist. Multiple sequencers per procedure
|
|
26042
|
+
* are independent — no shared cursor state.
|
|
26043
|
+
*/
|
|
26044
|
+
async openSequencer(procedureId) {
|
|
26045
|
+
const proc = await this.getProcedure(procedureId);
|
|
26046
|
+
if (!proc) return null;
|
|
26047
|
+
return new StepSequencer(proc);
|
|
26048
|
+
}
|
|
26049
|
+
/**
|
|
26050
|
+
* Token-overlap match: scores each procedure by Jaccard-like overlap
|
|
26051
|
+
* between the lowercased context tokens and the union of (`name`,
|
|
26052
|
+
* `triggers`). Returns matches in score order, descending. `threshold`
|
|
26053
|
+
* filters out matches below the cutoff (default 0.0 — return all).
|
|
26054
|
+
*/
|
|
26055
|
+
async matchProcedure(contextDescription, candidates, threshold = 0) {
|
|
26056
|
+
const ctxTokens = tokenize6(contextDescription);
|
|
26057
|
+
if (ctxTokens.size === 0) return [];
|
|
26058
|
+
const matches = [];
|
|
26059
|
+
for (const procedure of candidates) {
|
|
26060
|
+
const procTokens = /* @__PURE__ */ new Set();
|
|
26061
|
+
for (const t of tokenize6(procedure.name)) procTokens.add(t);
|
|
26062
|
+
for (const trig of procedure.triggers ?? []) {
|
|
26063
|
+
for (const t of tokenize6(trig)) procTokens.add(t);
|
|
26064
|
+
}
|
|
26065
|
+
if (procTokens.size === 0) continue;
|
|
26066
|
+
const intersection = /* @__PURE__ */ new Set();
|
|
26067
|
+
for (const t of ctxTokens) if (procTokens.has(t)) intersection.add(t);
|
|
26068
|
+
const union = /* @__PURE__ */ new Set([...ctxTokens, ...procTokens]);
|
|
26069
|
+
const score = intersection.size / union.size;
|
|
26070
|
+
if (score >= threshold) matches.push({ procedure, score });
|
|
26071
|
+
}
|
|
26072
|
+
matches.sort((a, b) => b.score - a.score);
|
|
26073
|
+
return matches;
|
|
26074
|
+
}
|
|
26075
|
+
/**
|
|
26076
|
+
* Apply caller feedback: increment `executionCount` and update
|
|
26077
|
+
* `successRate` via EWMA. Persists the updated procedure. Returns
|
|
26078
|
+
* the updated record. Throws if procedure does not exist.
|
|
26079
|
+
*/
|
|
26080
|
+
async refineProcedure(procedureId, feedback) {
|
|
26081
|
+
const proc = await this.getProcedure(procedureId);
|
|
26082
|
+
if (!proc) {
|
|
26083
|
+
throw new Error(`Procedure '${procedureId}' not found`);
|
|
26084
|
+
}
|
|
26085
|
+
const previousRate = proc.successRate ?? 0;
|
|
26086
|
+
const observation = feedback.succeeded ? 1 : 0;
|
|
26087
|
+
const baseline = (proc.executionCount ?? 0) === 0 ? 0.5 : previousRate;
|
|
26088
|
+
const newRate = baseline + this.successRateAlpha * (observation - baseline);
|
|
26089
|
+
const updated = {
|
|
26090
|
+
...proc,
|
|
26091
|
+
successRate: clamp01(newRate),
|
|
26092
|
+
executionCount: (proc.executionCount ?? 0) + 1,
|
|
26093
|
+
lastModified: feedback.recordedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
26094
|
+
};
|
|
26095
|
+
await this.store.update(updated);
|
|
26096
|
+
return updated;
|
|
26097
|
+
}
|
|
26098
|
+
};
|
|
26099
|
+
}
|
|
26100
|
+
});
|
|
26101
|
+
|
|
26102
|
+
// src/agent/causal/CausalReasoner.ts
|
|
26103
|
+
function chainScore(relations) {
|
|
26104
|
+
let score = 1;
|
|
26105
|
+
for (const r of relations) {
|
|
26106
|
+
const meta = r.metadata;
|
|
26107
|
+
const strength = typeof meta?.causalStrength === "number" ? meta.causalStrength : 1;
|
|
26108
|
+
score *= strength;
|
|
26109
|
+
}
|
|
26110
|
+
return score;
|
|
26111
|
+
}
|
|
26112
|
+
var DEFAULT_CAUSAL_RELATION_TYPES, CausalReasoner;
|
|
26113
|
+
var init_CausalReasoner = __esm({
|
|
26114
|
+
"src/agent/causal/CausalReasoner.ts"() {
|
|
26115
|
+
"use strict";
|
|
26116
|
+
init_esm_shims();
|
|
26117
|
+
DEFAULT_CAUSAL_RELATION_TYPES = [
|
|
26118
|
+
"causes",
|
|
26119
|
+
"enables",
|
|
26120
|
+
"prevents",
|
|
26121
|
+
"precedes",
|
|
26122
|
+
"correlates"
|
|
26123
|
+
];
|
|
26124
|
+
CausalReasoner = class {
|
|
26125
|
+
constructor(traversal, config = {}) {
|
|
26126
|
+
this.traversal = traversal;
|
|
26127
|
+
this.causalTypes = config.causalTypes ?? DEFAULT_CAUSAL_RELATION_TYPES;
|
|
26128
|
+
this.maxDepth = config.maxDepth ?? 6;
|
|
26129
|
+
}
|
|
26130
|
+
causalTypes;
|
|
26131
|
+
maxDepth;
|
|
26132
|
+
/**
|
|
26133
|
+
* Find all causal chains ending at `effectEntityName`. Searches for
|
|
26134
|
+
* paths from any node to `effectEntityName` along causal edges. In
|
|
26135
|
+
* practice we delegate to `findAllPaths` per candidate cause; for
|
|
26136
|
+
* unbounded discovery the caller should layer their own seed selection.
|
|
26137
|
+
*
|
|
26138
|
+
* Returns an empty array when no causal chain reaches the target. Each
|
|
26139
|
+
* chain's `score` is the product of `causalStrength` annotations on
|
|
26140
|
+
* its relations (defaults to 1 per edge when missing).
|
|
26141
|
+
*/
|
|
26142
|
+
async findCauses(effectEntityName, candidateCauses, maxDepth) {
|
|
26143
|
+
const depth = maxDepth ?? this.maxDepth;
|
|
26144
|
+
const chains = [];
|
|
26145
|
+
for (const cause of candidateCauses) {
|
|
26146
|
+
const paths = await this.traversal.findAllPaths(cause, effectEntityName, depth, {
|
|
26147
|
+
relationTypes: this.causalTypes
|
|
26148
|
+
});
|
|
26149
|
+
for (const p of paths) {
|
|
26150
|
+
chains.push({
|
|
26151
|
+
path: p.path,
|
|
26152
|
+
relations: p.relations,
|
|
26153
|
+
score: chainScore(p.relations),
|
|
26154
|
+
length: p.relations.length
|
|
26155
|
+
});
|
|
26156
|
+
}
|
|
26157
|
+
}
|
|
26158
|
+
chains.sort((a, b) => b.score - a.score);
|
|
26159
|
+
return chains;
|
|
26160
|
+
}
|
|
26161
|
+
/**
|
|
26162
|
+
* Find all causal chains starting at `causeEntityName` and reaching
|
|
26163
|
+
* any of `candidateEffects`. Symmetric counterpart to `findCauses`.
|
|
26164
|
+
*/
|
|
26165
|
+
async findEffects(causeEntityName, candidateEffects, maxDepth) {
|
|
26166
|
+
const depth = maxDepth ?? this.maxDepth;
|
|
26167
|
+
const chains = [];
|
|
26168
|
+
for (const effect of candidateEffects) {
|
|
26169
|
+
const paths = await this.traversal.findAllPaths(causeEntityName, effect, depth, {
|
|
26170
|
+
relationTypes: this.causalTypes
|
|
26171
|
+
});
|
|
26172
|
+
for (const p of paths) {
|
|
26173
|
+
chains.push({
|
|
26174
|
+
path: p.path,
|
|
26175
|
+
relations: p.relations,
|
|
26176
|
+
score: chainScore(p.relations),
|
|
26177
|
+
length: p.relations.length
|
|
26178
|
+
});
|
|
26179
|
+
}
|
|
26180
|
+
}
|
|
26181
|
+
chains.sort((a, b) => b.score - a.score);
|
|
26182
|
+
return chains;
|
|
26183
|
+
}
|
|
26184
|
+
/**
|
|
26185
|
+
* Counterfactual: "what changes if we remove edge `(removeFrom →
|
|
26186
|
+
* removeTo)` and ask whether `predict` is still reachable from
|
|
26187
|
+
* `seed`?" Returns chains from `seed` to `predict` that DO NOT use
|
|
26188
|
+
* the removed edge. Compare against the unfiltered `findEffects`
|
|
26189
|
+
* result to see which chains the removal kills.
|
|
26190
|
+
*
|
|
26191
|
+
* Pure: does not mutate the underlying graph or storage.
|
|
26192
|
+
*/
|
|
26193
|
+
async counterfactual(scenario) {
|
|
26194
|
+
const depth = scenario.maxDepth ?? this.maxDepth;
|
|
26195
|
+
const paths = await this.traversal.findAllPaths(
|
|
26196
|
+
scenario.seed,
|
|
26197
|
+
scenario.predict,
|
|
26198
|
+
depth,
|
|
26199
|
+
{ relationTypes: this.causalTypes }
|
|
26200
|
+
);
|
|
26201
|
+
const surviving = paths.filter(
|
|
26202
|
+
(p) => !p.relations.some(
|
|
26203
|
+
(r) => r.from === scenario.removeFrom && r.to === scenario.removeTo
|
|
26204
|
+
)
|
|
26205
|
+
);
|
|
26206
|
+
return surviving.map((p) => ({
|
|
26207
|
+
path: p.path,
|
|
26208
|
+
relations: p.relations,
|
|
26209
|
+
score: chainScore(p.relations),
|
|
26210
|
+
length: p.relations.length
|
|
26211
|
+
}));
|
|
26212
|
+
}
|
|
26213
|
+
/**
|
|
26214
|
+
* Detect cycles in the causal subgraph rooted at `seed`. Returns each
|
|
26215
|
+
* cycle as a list of entity names (with the repeating node at both
|
|
26216
|
+
* ends) plus the edges that close the loop.
|
|
26217
|
+
*
|
|
26218
|
+
* **Caveat**: treats `prevents` as a directed causal edge, NOT as a
|
|
26219
|
+
* negation. A `prevents`→`enables`→`prevents` triangle WILL show up
|
|
26220
|
+
* as a cycle. Document explicitly so callers don't misinterpret.
|
|
26221
|
+
*
|
|
26222
|
+
* Cycle detection here is a depth-bounded DFS rather than full Tarjan
|
|
26223
|
+
* SCC — sufficient for sparse causal graphs at hop counts ≤ 6, but
|
|
26224
|
+
* may double-report cycles that share edges. Filter by `cycle[0]`
|
|
26225
|
+
* sort-then-stringify if exact dedup is needed.
|
|
26226
|
+
*/
|
|
26227
|
+
detectCycles(seed, maxDepth) {
|
|
26228
|
+
const depth = maxDepth ?? this.maxDepth;
|
|
26229
|
+
const cycles = [];
|
|
26230
|
+
const path6 = [];
|
|
26231
|
+
const relations = [];
|
|
26232
|
+
const inPath = /* @__PURE__ */ new Set();
|
|
26233
|
+
const dfs = (node, d) => {
|
|
26234
|
+
if (d > depth) return;
|
|
26235
|
+
inPath.add(node);
|
|
26236
|
+
path6.push(node);
|
|
26237
|
+
const neighbors = this.traversal.getNeighborsWithRelations(node, {
|
|
26238
|
+
relationTypes: this.causalTypes,
|
|
26239
|
+
direction: "outgoing"
|
|
26240
|
+
});
|
|
26241
|
+
for (const { neighbor, relation } of neighbors) {
|
|
26242
|
+
if (inPath.has(neighbor)) {
|
|
26243
|
+
const idx = path6.indexOf(neighbor);
|
|
26244
|
+
if (idx >= 0) {
|
|
26245
|
+
cycles.push({
|
|
26246
|
+
cycle: [...path6.slice(idx), neighbor],
|
|
26247
|
+
relations: [...relations.slice(idx), relation]
|
|
26248
|
+
});
|
|
26249
|
+
}
|
|
26250
|
+
} else {
|
|
26251
|
+
relations.push(relation);
|
|
26252
|
+
dfs(neighbor, d + 1);
|
|
26253
|
+
relations.pop();
|
|
26254
|
+
}
|
|
26255
|
+
}
|
|
26256
|
+
inPath.delete(node);
|
|
26257
|
+
path6.pop();
|
|
26258
|
+
};
|
|
26259
|
+
dfs(seed, 0);
|
|
26260
|
+
return cycles;
|
|
26261
|
+
}
|
|
26262
|
+
};
|
|
26263
|
+
}
|
|
26264
|
+
});
|
|
26265
|
+
|
|
26266
|
+
// src/agent/rbac/PermissionMatrix.ts
|
|
26267
|
+
function permissionsForRole(role, resourceType, matrix = DEFAULT_PERMISSION_MATRIX, overrides) {
|
|
26268
|
+
const overrideMatrix = overrides?.get(resourceType);
|
|
26269
|
+
if (overrideMatrix?.has(role)) {
|
|
26270
|
+
return overrideMatrix.get(role);
|
|
26271
|
+
}
|
|
26272
|
+
return matrix.get(role) ?? [];
|
|
26273
|
+
}
|
|
26274
|
+
var DEFAULT_PERMISSION_MATRIX;
|
|
26275
|
+
var init_PermissionMatrix = __esm({
|
|
26276
|
+
"src/agent/rbac/PermissionMatrix.ts"() {
|
|
26277
|
+
"use strict";
|
|
26278
|
+
init_esm_shims();
|
|
26279
|
+
DEFAULT_PERMISSION_MATRIX = /* @__PURE__ */ new Map([
|
|
26280
|
+
["reader", ["read"]],
|
|
26281
|
+
["writer", ["read", "write"]],
|
|
26282
|
+
["admin", ["read", "write", "delete"]],
|
|
26283
|
+
["owner", ["read", "write", "delete", "manage"]]
|
|
26284
|
+
]);
|
|
26285
|
+
}
|
|
26286
|
+
});
|
|
26287
|
+
|
|
26288
|
+
// src/agent/rbac/RbacMiddleware.ts
|
|
26289
|
+
var RbacMiddleware;
|
|
26290
|
+
var init_RbacMiddleware = __esm({
|
|
26291
|
+
"src/agent/rbac/RbacMiddleware.ts"() {
|
|
26292
|
+
"use strict";
|
|
26293
|
+
init_esm_shims();
|
|
26294
|
+
init_PermissionMatrix();
|
|
26295
|
+
RbacMiddleware = class {
|
|
26296
|
+
constructor(store, options) {
|
|
26297
|
+
this.store = store;
|
|
26298
|
+
this.matrix = options?.matrix ?? DEFAULT_PERMISSION_MATRIX;
|
|
26299
|
+
this.overrides = options?.overrides;
|
|
26300
|
+
this.defaultRole = options?.defaultRole === void 0 && options !== void 0 ? options.defaultRole : options?.defaultRole ?? "reader";
|
|
26301
|
+
}
|
|
26302
|
+
matrix;
|
|
26303
|
+
overrides;
|
|
26304
|
+
defaultRole;
|
|
26305
|
+
checkPermission(agentId, action, resourceType, resourceName, now) {
|
|
26306
|
+
const active = this.store.listActive(agentId, now);
|
|
26307
|
+
const applicable = active.filter((a) => this.matchesResource(a, resourceType, resourceName));
|
|
26308
|
+
if (applicable.length === 0) {
|
|
26309
|
+
if (!this.defaultRole) return false;
|
|
26310
|
+
const granted = permissionsForRole(this.defaultRole, resourceType, this.matrix, this.overrides);
|
|
26311
|
+
return granted.includes(action);
|
|
26312
|
+
}
|
|
26313
|
+
return applicable.some((a) => {
|
|
26314
|
+
const granted = permissionsForRole(a.role, resourceType, this.matrix, this.overrides);
|
|
26315
|
+
return granted.includes(action);
|
|
26316
|
+
});
|
|
26317
|
+
}
|
|
26318
|
+
matchesResource(assignment, resourceType, resourceName) {
|
|
26319
|
+
if (assignment.resourceType !== void 0 && assignment.resourceType !== resourceType) {
|
|
26320
|
+
return false;
|
|
26321
|
+
}
|
|
26322
|
+
if (assignment.scope) {
|
|
26323
|
+
if (!resourceName) return false;
|
|
26324
|
+
if (!resourceName.startsWith(assignment.scope)) return false;
|
|
26325
|
+
}
|
|
26326
|
+
return true;
|
|
26327
|
+
}
|
|
26328
|
+
};
|
|
26329
|
+
}
|
|
26330
|
+
});
|
|
26331
|
+
|
|
26332
|
+
// src/agent/rbac/RoleAssignmentStore.ts
|
|
26333
|
+
import { promises as fs9 } from "fs";
|
|
26334
|
+
var RoleAssignmentStore;
|
|
26335
|
+
var init_RoleAssignmentStore = __esm({
|
|
26336
|
+
"src/agent/rbac/RoleAssignmentStore.ts"() {
|
|
26337
|
+
"use strict";
|
|
26338
|
+
init_esm_shims();
|
|
26339
|
+
RoleAssignmentStore = class {
|
|
26340
|
+
assignments = /* @__PURE__ */ new Map();
|
|
26341
|
+
persistencePath;
|
|
26342
|
+
constructor(options) {
|
|
26343
|
+
this.persistencePath = options?.persistencePath;
|
|
26344
|
+
}
|
|
26345
|
+
/**
|
|
26346
|
+
* Replay the JSONL persistence file (if configured) into the in-memory
|
|
26347
|
+
* map. Idempotent — safe to call multiple times. No-op when no path
|
|
26348
|
+
* is set or the file does not exist.
|
|
26349
|
+
*/
|
|
26350
|
+
async hydrate() {
|
|
26351
|
+
if (!this.persistencePath) return;
|
|
26352
|
+
let content;
|
|
26353
|
+
try {
|
|
26354
|
+
content = await fs9.readFile(this.persistencePath, "utf-8");
|
|
26355
|
+
} catch (e) {
|
|
26356
|
+
if (e.code === "ENOENT") return;
|
|
26357
|
+
throw e;
|
|
26358
|
+
}
|
|
26359
|
+
this.assignments.clear();
|
|
26360
|
+
for (const line of content.split("\n")) {
|
|
26361
|
+
if (!line.trim()) continue;
|
|
26362
|
+
try {
|
|
26363
|
+
const rec = JSON.parse(line);
|
|
26364
|
+
if (rec.op === "assign") {
|
|
26365
|
+
this.applyAssign(rec.assignment);
|
|
26366
|
+
} else {
|
|
26367
|
+
this.applyRevoke(rec.agentId, rec.role, rec.resourceType);
|
|
26368
|
+
}
|
|
26369
|
+
} catch {
|
|
26370
|
+
}
|
|
26371
|
+
}
|
|
26372
|
+
}
|
|
26373
|
+
/**
|
|
26374
|
+
* Add an assignment. Multiple grants per agent are allowed (e.g. one
|
|
26375
|
+
* agent may be a `reader` for entities and a `writer` for relations).
|
|
26376
|
+
* Persists if configured.
|
|
26377
|
+
*/
|
|
26378
|
+
async assign(assignment) {
|
|
26379
|
+
this.applyAssign(assignment);
|
|
26380
|
+
await this.persist({ op: "assign", assignment, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
26381
|
+
}
|
|
26382
|
+
/**
|
|
26383
|
+
* Remove a specific assignment. Matching is by `agentId + role +
|
|
26384
|
+
* resourceType` (the resourceType match is exact, including undefined).
|
|
26385
|
+
*/
|
|
26386
|
+
async revoke(agentId, role, resourceType) {
|
|
26387
|
+
this.applyRevoke(agentId, role, resourceType);
|
|
26388
|
+
await this.persist({ op: "revoke", agentId, role, resourceType, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
26389
|
+
}
|
|
26390
|
+
/** All assignments for the given agent (active and inactive). */
|
|
26391
|
+
list(agentId) {
|
|
26392
|
+
return this.assignments.get(agentId)?.slice() ?? [];
|
|
26393
|
+
}
|
|
26394
|
+
/**
|
|
26395
|
+
* Active assignments for the given agent at the supplied time. Default
|
|
26396
|
+
* is current time. An assignment is active when `validFrom <= now <=
|
|
26397
|
+
* validUntil` (with absent bounds treated as unbounded).
|
|
26398
|
+
*/
|
|
26399
|
+
listActive(agentId, now) {
|
|
26400
|
+
const ts = now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
26401
|
+
return this.list(agentId).filter((a) => {
|
|
26402
|
+
if (a.validFrom && a.validFrom > ts) return false;
|
|
26403
|
+
if (a.validUntil && a.validUntil < ts) return false;
|
|
26404
|
+
return true;
|
|
26405
|
+
});
|
|
26406
|
+
}
|
|
26407
|
+
// -------- Internal --------
|
|
26408
|
+
applyAssign(assignment) {
|
|
26409
|
+
const list = this.assignments.get(assignment.agentId) ?? [];
|
|
26410
|
+
list.push(assignment);
|
|
26411
|
+
this.assignments.set(assignment.agentId, list);
|
|
26412
|
+
}
|
|
26413
|
+
applyRevoke(agentId, role, resourceType) {
|
|
26414
|
+
const list = this.assignments.get(agentId);
|
|
26415
|
+
if (!list) return;
|
|
26416
|
+
const filtered = list.filter(
|
|
26417
|
+
(a) => !(a.role === role && a.resourceType === resourceType)
|
|
26418
|
+
);
|
|
26419
|
+
if (filtered.length === 0) {
|
|
26420
|
+
this.assignments.delete(agentId);
|
|
26421
|
+
} else {
|
|
26422
|
+
this.assignments.set(agentId, filtered);
|
|
26423
|
+
}
|
|
26424
|
+
}
|
|
26425
|
+
async persist(record) {
|
|
26426
|
+
if (!this.persistencePath) return;
|
|
26427
|
+
const line = JSON.stringify(record) + "\n";
|
|
26428
|
+
await fs9.appendFile(this.persistencePath, line, "utf-8");
|
|
26429
|
+
}
|
|
26430
|
+
};
|
|
26431
|
+
}
|
|
26432
|
+
});
|
|
26433
|
+
|
|
26434
|
+
// src/agent/world/WorldStateSnapshot.ts
|
|
26435
|
+
function diffFields(before, after) {
|
|
26436
|
+
const out = [];
|
|
26437
|
+
if (before.entityType !== after.entityType) out.push("entityType");
|
|
26438
|
+
if (before.importance !== after.importance) out.push("importance");
|
|
26439
|
+
if (before.confidence !== after.confidence) out.push("confidence");
|
|
26440
|
+
if (before.observationCount !== after.observationCount) out.push("observationCount");
|
|
26441
|
+
if (!sameStringSet(before.tags, after.tags)) out.push("tags");
|
|
26442
|
+
if (before.lastModified !== after.lastModified) out.push("lastModified");
|
|
26443
|
+
return out;
|
|
26444
|
+
}
|
|
26445
|
+
function sameStringSet(a, b) {
|
|
26446
|
+
if (a.length !== b.length) return false;
|
|
26447
|
+
const setA = new Set(a);
|
|
26448
|
+
for (const x of b) if (!setA.has(x)) return false;
|
|
26449
|
+
return true;
|
|
26450
|
+
}
|
|
26451
|
+
var WorldStateSnapshot;
|
|
26452
|
+
var init_WorldStateSnapshot = __esm({
|
|
26453
|
+
"src/agent/world/WorldStateSnapshot.ts"() {
|
|
26454
|
+
"use strict";
|
|
26455
|
+
init_esm_shims();
|
|
26456
|
+
WorldStateSnapshot = class _WorldStateSnapshot {
|
|
26457
|
+
/** ISO 8601 timestamp this snapshot was taken. */
|
|
26458
|
+
takenAt;
|
|
26459
|
+
/** Map keyed by entity name. */
|
|
26460
|
+
entitiesByName;
|
|
26461
|
+
constructor(entities, takenAt) {
|
|
26462
|
+
this.takenAt = takenAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
26463
|
+
const m = /* @__PURE__ */ new Map();
|
|
26464
|
+
for (const e of entities) m.set(e.name, e);
|
|
26465
|
+
this.entitiesByName = m;
|
|
26466
|
+
}
|
|
26467
|
+
/** Number of entities in the snapshot. */
|
|
26468
|
+
get size() {
|
|
26469
|
+
return this.entitiesByName.size;
|
|
26470
|
+
}
|
|
26471
|
+
/** All entities, in insertion order. */
|
|
26472
|
+
entities() {
|
|
26473
|
+
return [...this.entitiesByName.values()];
|
|
26474
|
+
}
|
|
26475
|
+
/**
|
|
26476
|
+
* Pure: compute the diff to a `next` snapshot. Returns added /
|
|
26477
|
+
* removed / modified breakdown. An entity counts as "modified" when
|
|
26478
|
+
* any of `importance`, `confidence`, `observationCount`, `tags`, or
|
|
26479
|
+
* `lastModified` differs.
|
|
26480
|
+
*/
|
|
26481
|
+
diffTo(next) {
|
|
26482
|
+
const removed = [];
|
|
26483
|
+
const added = [];
|
|
26484
|
+
const modified = [];
|
|
26485
|
+
for (const [name, before] of this.entitiesByName) {
|
|
26486
|
+
const after = next.entitiesByName.get(name);
|
|
26487
|
+
if (!after) {
|
|
26488
|
+
removed.push(before);
|
|
26489
|
+
continue;
|
|
26490
|
+
}
|
|
26491
|
+
const fields = diffFields(before, after);
|
|
26492
|
+
if (fields.length > 0) {
|
|
26493
|
+
modified.push({ name, before, after, fields });
|
|
26494
|
+
}
|
|
26495
|
+
}
|
|
26496
|
+
for (const [name, after] of next.entitiesByName) {
|
|
26497
|
+
if (!this.entitiesByName.has(name)) added.push(after);
|
|
26498
|
+
}
|
|
26499
|
+
return { removed, added, modified };
|
|
26500
|
+
}
|
|
26501
|
+
/** JSON-serializable form. */
|
|
26502
|
+
toJSON() {
|
|
26503
|
+
return { takenAt: this.takenAt, entities: [...this.entities()] };
|
|
26504
|
+
}
|
|
26505
|
+
/** Reconstruct from `toJSON()` output. */
|
|
26506
|
+
static fromJSON(json) {
|
|
26507
|
+
return new _WorldStateSnapshot(json.entities, json.takenAt);
|
|
26508
|
+
}
|
|
26509
|
+
};
|
|
26510
|
+
}
|
|
26511
|
+
});
|
|
26512
|
+
|
|
26513
|
+
// src/agent/world/WorldModelManager.ts
|
|
26514
|
+
var WorldModelManager;
|
|
26515
|
+
var init_WorldModelManager = __esm({
|
|
26516
|
+
"src/agent/world/WorldModelManager.ts"() {
|
|
26517
|
+
"use strict";
|
|
26518
|
+
init_esm_shims();
|
|
26519
|
+
init_WorldStateSnapshot();
|
|
26520
|
+
WorldModelManager = class {
|
|
26521
|
+
constructor(entityManager, causalReasoner, memoryValidator, options = {}) {
|
|
26522
|
+
this.entityManager = entityManager;
|
|
26523
|
+
this.causalReasoner = causalReasoner;
|
|
26524
|
+
this.memoryValidator = memoryValidator;
|
|
26525
|
+
this.maxSnapshotSize = options.maxSnapshotSize ?? 1e3;
|
|
26526
|
+
}
|
|
26527
|
+
maxSnapshotSize;
|
|
26528
|
+
/**
|
|
26529
|
+
* Build a fresh snapshot from the live graph. Loads ALL entities (capped
|
|
26530
|
+
* at `maxSnapshotSize`) and reduces each to a `WorldStateEntity`. Pure
|
|
26531
|
+
* reads; safe to call concurrently.
|
|
26532
|
+
*
|
|
26533
|
+
* For graphs larger than the cap, entities are sorted by `importance`
|
|
26534
|
+
* descending and truncated — high-importance entities preferred.
|
|
26535
|
+
*/
|
|
26536
|
+
async getCurrentState() {
|
|
26537
|
+
const graph = await this.entityManager["storage"].loadGraph();
|
|
26538
|
+
let entities = graph.entities;
|
|
26539
|
+
if (entities.length > this.maxSnapshotSize) {
|
|
26540
|
+
entities = [...entities].sort((a, b) => (b.importance ?? 0) - (a.importance ?? 0)).slice(0, this.maxSnapshotSize);
|
|
26541
|
+
}
|
|
26542
|
+
const snapshotEntities = entities.map((e) => ({
|
|
26543
|
+
name: e.name,
|
|
26544
|
+
entityType: e.entityType,
|
|
26545
|
+
importance: e.importance,
|
|
26546
|
+
confidence: e.confidence,
|
|
26547
|
+
observationCount: e.observations.length,
|
|
26548
|
+
tags: [...e.tags ?? []],
|
|
26549
|
+
lastModified: e.lastModified
|
|
26550
|
+
}));
|
|
26551
|
+
return new WorldStateSnapshot(snapshotEntities);
|
|
26552
|
+
}
|
|
26553
|
+
/**
|
|
26554
|
+
* Validate a candidate observation against the named entity's current
|
|
26555
|
+
* state. Delegates to `MemoryValidator.validateConsistency` when one
|
|
26556
|
+
* was wired at construction; returns a deferred result with a `null`
|
|
26557
|
+
* `issues` array when no validator is available — callers should treat
|
|
26558
|
+
* `valid: undefined` as "not checked" rather than "passed".
|
|
26559
|
+
*/
|
|
26560
|
+
async validateFact(observation, entityName) {
|
|
26561
|
+
if (!this.memoryValidator) return null;
|
|
26562
|
+
const entity = await this.entityManager.getEntity(entityName);
|
|
26563
|
+
if (!entity) return null;
|
|
26564
|
+
return this.memoryValidator.validateConsistency(observation, entity);
|
|
26565
|
+
}
|
|
26566
|
+
/**
|
|
26567
|
+
* Predict downstream effects of an action by walking the causal
|
|
26568
|
+
* subgraph from `actionEntity` to each candidate effect. Returns
|
|
26569
|
+
* empty when no causal reasoner was wired or no chain reaches any
|
|
26570
|
+
* candidate.
|
|
26571
|
+
*/
|
|
26572
|
+
async predictOutcome(actionEntity, candidateEffects) {
|
|
26573
|
+
if (!this.causalReasoner) return [];
|
|
26574
|
+
return this.causalReasoner.findEffects(actionEntity, candidateEffects);
|
|
26575
|
+
}
|
|
26576
|
+
/**
|
|
26577
|
+
* Pure: diff two snapshots. Direct passthrough to
|
|
26578
|
+
* `WorldStateSnapshot.diffTo` — exposed here so callers can use the
|
|
26579
|
+
* world-model facade for both snapshotting and change detection.
|
|
26580
|
+
*/
|
|
26581
|
+
detectStateChange(before, after) {
|
|
26582
|
+
return before.diffTo(after);
|
|
26583
|
+
}
|
|
26584
|
+
};
|
|
26585
|
+
}
|
|
26586
|
+
});
|
|
26587
|
+
|
|
26588
|
+
// src/agent/retrieval/QueryRewriter.ts
|
|
26589
|
+
function tokenize7(s) {
|
|
26590
|
+
return s.toLowerCase().split(/[^a-z0-9]+/g).filter((t) => t.length >= 3 && !STOPWORDS3.has(t));
|
|
26591
|
+
}
|
|
26592
|
+
var STOPWORDS3, QueryRewriter;
|
|
26593
|
+
var init_QueryRewriter = __esm({
|
|
26594
|
+
"src/agent/retrieval/QueryRewriter.ts"() {
|
|
26595
|
+
"use strict";
|
|
26596
|
+
init_esm_shims();
|
|
26597
|
+
STOPWORDS3 = /* @__PURE__ */ new Set([
|
|
26598
|
+
"the",
|
|
26599
|
+
"a",
|
|
26600
|
+
"an",
|
|
26601
|
+
"of",
|
|
26602
|
+
"and",
|
|
26603
|
+
"or",
|
|
26604
|
+
"but",
|
|
26605
|
+
"is",
|
|
26606
|
+
"are",
|
|
26607
|
+
"was",
|
|
26608
|
+
"were",
|
|
26609
|
+
"be",
|
|
26610
|
+
"been",
|
|
26611
|
+
"being",
|
|
26612
|
+
"have",
|
|
26613
|
+
"has",
|
|
26614
|
+
"had",
|
|
26615
|
+
"do",
|
|
26616
|
+
"does",
|
|
26617
|
+
"did",
|
|
26618
|
+
"will",
|
|
26619
|
+
"would",
|
|
26620
|
+
"could",
|
|
26621
|
+
"should",
|
|
26622
|
+
"may",
|
|
26623
|
+
"might",
|
|
26624
|
+
"must",
|
|
26625
|
+
"shall",
|
|
26626
|
+
"can",
|
|
26627
|
+
"to",
|
|
26628
|
+
"for",
|
|
26629
|
+
"with",
|
|
26630
|
+
"on",
|
|
26631
|
+
"in",
|
|
26632
|
+
"at",
|
|
26633
|
+
"by",
|
|
26634
|
+
"from",
|
|
26635
|
+
"up",
|
|
26636
|
+
"about",
|
|
26637
|
+
"as",
|
|
26638
|
+
"this",
|
|
26639
|
+
"that",
|
|
26640
|
+
"these",
|
|
26641
|
+
"those",
|
|
26642
|
+
"i",
|
|
26643
|
+
"you",
|
|
26644
|
+
"he",
|
|
26645
|
+
"she",
|
|
26646
|
+
"it",
|
|
26647
|
+
"we",
|
|
26648
|
+
"they",
|
|
26649
|
+
"his",
|
|
26650
|
+
"her",
|
|
26651
|
+
"its",
|
|
26652
|
+
"our",
|
|
26653
|
+
"their",
|
|
26654
|
+
"me",
|
|
26655
|
+
"him",
|
|
26656
|
+
"them",
|
|
26657
|
+
"us",
|
|
26658
|
+
"my",
|
|
26659
|
+
"your"
|
|
26660
|
+
]);
|
|
26661
|
+
QueryRewriter = class {
|
|
26662
|
+
/**
|
|
26663
|
+
* Expand `query` with the top-`expansionLimit` co-occurring tokens
|
|
26664
|
+
* from `snippets`. Tokens already present in the query (case-
|
|
26665
|
+
* insensitive) and stopwords are excluded.
|
|
26666
|
+
*/
|
|
26667
|
+
rewrite(query, snippets, expansionLimit = 3) {
|
|
26668
|
+
const queryTokens = new Set(tokenize7(query));
|
|
26669
|
+
const counts = /* @__PURE__ */ new Map();
|
|
26670
|
+
for (const s of snippets) {
|
|
26671
|
+
const seen = /* @__PURE__ */ new Set();
|
|
26672
|
+
for (const t of tokenize7(s)) {
|
|
26673
|
+
if (queryTokens.has(t)) continue;
|
|
26674
|
+
if (seen.has(t)) continue;
|
|
26675
|
+
seen.add(t);
|
|
26676
|
+
counts.set(t, (counts.get(t) ?? 0) + 1);
|
|
26677
|
+
}
|
|
26678
|
+
}
|
|
26679
|
+
const top = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, expansionLimit).map(([t]) => t);
|
|
26680
|
+
if (top.length === 0) {
|
|
26681
|
+
return { query, expansionTokens: [] };
|
|
26682
|
+
}
|
|
26683
|
+
return {
|
|
26684
|
+
query: `${query} ${top.join(" ")}`,
|
|
26685
|
+
expansionTokens: top
|
|
26686
|
+
};
|
|
26687
|
+
}
|
|
26688
|
+
};
|
|
26689
|
+
}
|
|
26690
|
+
});
|
|
26691
|
+
|
|
26692
|
+
// src/agent/retrieval/ActiveRetrievalController.ts
|
|
26693
|
+
var ActiveRetrievalController;
|
|
26694
|
+
var init_ActiveRetrievalController = __esm({
|
|
26695
|
+
"src/agent/retrieval/ActiveRetrievalController.ts"() {
|
|
26696
|
+
"use strict";
|
|
26697
|
+
init_esm_shims();
|
|
26698
|
+
init_QueryRewriter();
|
|
26699
|
+
ActiveRetrievalController = class {
|
|
26700
|
+
constructor(rankedSearch, config = {}) {
|
|
26701
|
+
this.rankedSearch = rankedSearch;
|
|
26702
|
+
this.maxRounds = config.maxRounds ?? 3;
|
|
26703
|
+
this.minCoverage = config.minCoverage ?? 0.6;
|
|
26704
|
+
this.resultsPerRound = config.resultsPerRound ?? 10;
|
|
26705
|
+
this.costThreshold = config.costThreshold ?? 1e3;
|
|
26706
|
+
this.expansionLimit = config.expansionLimit ?? 3;
|
|
26707
|
+
}
|
|
26708
|
+
rewriter = new QueryRewriter();
|
|
26709
|
+
maxRounds;
|
|
26710
|
+
minCoverage;
|
|
26711
|
+
resultsPerRound;
|
|
26712
|
+
costThreshold;
|
|
26713
|
+
expansionLimit;
|
|
26714
|
+
/**
|
|
26715
|
+
* Decide whether retrieval is worth the cost. Currently a simple
|
|
26716
|
+
* heuristic: tokens(query) × resultsPerRound × maxRounds × constant.
|
|
26717
|
+
* Returns `retrieve: false` when estimated cost > budget OR query is
|
|
26718
|
+
* empty.
|
|
26719
|
+
*/
|
|
26720
|
+
shouldRetrieve(context) {
|
|
26721
|
+
const tokens = context.query.trim().split(/\s+/).filter(Boolean);
|
|
26722
|
+
if (tokens.length === 0) {
|
|
26723
|
+
return { retrieve: false, estimatedCost: 0, reason: "Empty query" };
|
|
26724
|
+
}
|
|
26725
|
+
const estimatedCost = tokens.length * 5 + this.resultsPerRound * this.maxRounds * 50;
|
|
26726
|
+
const budget = context.budgetTokens ?? this.costThreshold;
|
|
26727
|
+
if (estimatedCost > budget) {
|
|
26728
|
+
return {
|
|
26729
|
+
retrieve: false,
|
|
26730
|
+
estimatedCost,
|
|
26731
|
+
reason: `Estimated cost ${estimatedCost} exceeds budget ${budget}`
|
|
26732
|
+
};
|
|
26733
|
+
}
|
|
26734
|
+
return {
|
|
26735
|
+
retrieve: true,
|
|
26736
|
+
estimatedCost,
|
|
26737
|
+
reason: "Cost within budget"
|
|
26738
|
+
};
|
|
26739
|
+
}
|
|
26740
|
+
/**
|
|
26741
|
+
* Run up to `maxRounds` of (search → score coverage → rewrite). Stops
|
|
26742
|
+
* early when coverage hits `minCoverage` or no expansion tokens are
|
|
26743
|
+
* available. Returns the highest-coverage round's results plus the
|
|
26744
|
+
* full per-round trace.
|
|
26745
|
+
*/
|
|
26746
|
+
async adaptiveRetrieve(context) {
|
|
26747
|
+
const rounds = [];
|
|
26748
|
+
let currentQuery = context.query;
|
|
26749
|
+
let bestRound = null;
|
|
26750
|
+
for (let i = 0; i < this.maxRounds; i++) {
|
|
26751
|
+
const results = await this.rankedSearch.searchNodesRanked(
|
|
26752
|
+
currentQuery,
|
|
26753
|
+
void 0,
|
|
26754
|
+
void 0,
|
|
26755
|
+
void 0,
|
|
26756
|
+
this.resultsPerRound
|
|
26757
|
+
);
|
|
26758
|
+
const coverage = this.estimateCoverage(results);
|
|
26759
|
+
const round = {
|
|
26760
|
+
query: currentQuery,
|
|
26761
|
+
results,
|
|
26762
|
+
coverage,
|
|
26763
|
+
expansionTokens: i === 0 ? [] : rounds[i - 1].expansionTokens ?? []
|
|
26764
|
+
};
|
|
26765
|
+
rounds.push(round);
|
|
26766
|
+
if (!bestRound || coverage > bestRound.coverage) bestRound = round;
|
|
26767
|
+
if (coverage >= this.minCoverage) break;
|
|
26768
|
+
if (results.length === 0) break;
|
|
26769
|
+
const snippets = results.flatMap((r) => r.entity.observations).slice(0, 20);
|
|
26770
|
+
const rewrite = this.rewriter.rewrite(currentQuery, snippets, this.expansionLimit);
|
|
26771
|
+
if (rewrite.expansionTokens.length === 0) break;
|
|
26772
|
+
currentQuery = rewrite.query;
|
|
26773
|
+
rounds[rounds.length - 1].expansionTokens = rewrite.expansionTokens;
|
|
26774
|
+
}
|
|
26775
|
+
return {
|
|
26776
|
+
bestResults: bestRound?.results ?? [],
|
|
26777
|
+
bestCoverage: bestRound?.coverage ?? 0,
|
|
26778
|
+
rounds
|
|
26779
|
+
};
|
|
26780
|
+
}
|
|
26781
|
+
/**
|
|
26782
|
+
* Quick coverage estimate: average of the top-3 results' scores,
|
|
26783
|
+
* clamped to [0, 1]. Empty results → 0. Score normalization assumes
|
|
26784
|
+
* `RankedSearch` returns BM25-ish positive numbers; we cap at 1.0 to
|
|
26785
|
+
* keep the threshold comparison meaningful.
|
|
26786
|
+
*/
|
|
26787
|
+
estimateCoverage(results) {
|
|
26788
|
+
if (results.length === 0) return 0;
|
|
26789
|
+
const top = results.slice(0, 3);
|
|
26790
|
+
const avg = top.reduce((acc, r) => acc + (r.score ?? 0), 0) / top.length;
|
|
26791
|
+
return Math.min(1, Math.max(0, avg));
|
|
26792
|
+
}
|
|
26793
|
+
};
|
|
26794
|
+
}
|
|
26795
|
+
});
|
|
26796
|
+
|
|
26797
|
+
// src/core/ManagerContext.ts
|
|
26798
|
+
import path5 from "path";
|
|
26799
|
+
var ManagerContext;
|
|
26800
|
+
var init_ManagerContext = __esm({
|
|
26801
|
+
"src/core/ManagerContext.ts"() {
|
|
26802
|
+
"use strict";
|
|
26803
|
+
init_esm_shims();
|
|
26804
|
+
init_StorageFactory();
|
|
26805
|
+
init_EntityManager();
|
|
26806
|
+
init_RelationManager();
|
|
26807
|
+
init_ObservationManager();
|
|
26808
|
+
init_HierarchyManager();
|
|
26809
|
+
init_GraphTraversal();
|
|
26810
|
+
init_SearchManager();
|
|
26811
|
+
init_RankedSearch();
|
|
26812
|
+
init_LLMQueryPlanner();
|
|
26813
|
+
init_LLMSearchExecutor();
|
|
26814
|
+
init_search();
|
|
26815
|
+
init_IOManager();
|
|
26816
|
+
init_TagManager();
|
|
26817
|
+
init_AnalyticsManager();
|
|
26818
|
+
init_CompressionManager();
|
|
26819
|
+
init_ArchiveManager();
|
|
26820
|
+
init_AutoLinker();
|
|
26821
|
+
init_FactExtractor();
|
|
26822
|
+
init_TransitionLedger();
|
|
26823
|
+
init_AccessTracker();
|
|
26824
|
+
init_DecayEngine();
|
|
26825
|
+
init_DecayScheduler();
|
|
26826
|
+
init_ConsolidationScheduler();
|
|
26827
|
+
init_SalienceEngine();
|
|
26828
|
+
init_ContextWindowManager();
|
|
26829
|
+
init_MemoryFormatter();
|
|
26830
|
+
init_AgentMemoryManager();
|
|
26831
|
+
init_ArtifactManager();
|
|
26832
|
+
init_DreamEngine();
|
|
26833
|
+
init_RefIndex();
|
|
26834
|
+
init_ObserverPipeline();
|
|
26835
|
+
init_constants();
|
|
26836
|
+
init_utils();
|
|
26837
|
+
init_ContradictionDetector();
|
|
26838
|
+
init_SemanticForget();
|
|
26839
|
+
init_MemoryEngine();
|
|
26840
|
+
init_ImportanceScorer();
|
|
26841
|
+
init_InMemoryBackend();
|
|
26842
|
+
init_SQLiteBackend();
|
|
26843
|
+
init_MemoryValidator();
|
|
26844
|
+
init_TrajectoryCompressor();
|
|
26845
|
+
init_ExperienceExtractor();
|
|
26846
|
+
init_PatternDetector();
|
|
26847
|
+
init_ProcedureManager();
|
|
26848
|
+
init_CausalReasoner();
|
|
26849
|
+
init_RbacMiddleware();
|
|
26850
|
+
init_RoleAssignmentStore();
|
|
26851
|
+
init_WorldModelManager();
|
|
26852
|
+
init_ActiveRetrievalController();
|
|
26853
|
+
ManagerContext = class {
|
|
26854
|
+
// Type as GraphStorage for manager compatibility; actual instance may be SQLiteStorage
|
|
26855
|
+
// which implements the same interface via duck typing
|
|
26856
|
+
storage;
|
|
26857
|
+
defaultProjectId;
|
|
26858
|
+
savedSearchesFilePath;
|
|
26859
|
+
tagAliasesFilePath;
|
|
26860
|
+
refIndexFilePath;
|
|
26861
|
+
_observerPipeline;
|
|
26862
|
+
// ==================== LAZY-INITIALIZED CORE MANAGERS ====================
|
|
26863
|
+
_entityManager;
|
|
26864
|
+
_relationManager;
|
|
26865
|
+
_observationManager;
|
|
26866
|
+
_hierarchyManager;
|
|
26867
|
+
_graphTraversal;
|
|
26868
|
+
_searchManager;
|
|
26869
|
+
_rankedSearch;
|
|
26870
|
+
_ioManager;
|
|
26871
|
+
_tagManager;
|
|
26872
|
+
_analyticsManager;
|
|
26873
|
+
_compressionManager;
|
|
26874
|
+
_archiveManager;
|
|
26875
|
+
_autoLinker;
|
|
26876
|
+
_factExtractor;
|
|
26877
|
+
_transitionLedger;
|
|
26878
|
+
_semanticSearch;
|
|
26879
|
+
_memoryEngine;
|
|
26880
|
+
_memoryBackend;
|
|
26881
|
+
_memoryValidator;
|
|
26882
|
+
_trajectoryCompressor;
|
|
26883
|
+
_experienceExtractor;
|
|
26884
|
+
_patternDetector;
|
|
26885
|
+
_procedureManager;
|
|
26886
|
+
_causalReasoner;
|
|
26887
|
+
_rbacMiddleware;
|
|
26888
|
+
_roleAssignmentStore;
|
|
26889
|
+
_worldModelManager;
|
|
26890
|
+
_activeRetrieval;
|
|
26891
|
+
_accessTracker;
|
|
26892
|
+
_decayEngine;
|
|
26893
|
+
_decayScheduler;
|
|
26894
|
+
_salienceEngine;
|
|
26895
|
+
_contextWindowManager;
|
|
26896
|
+
_memoryFormatter;
|
|
26897
|
+
_agentMemory;
|
|
26898
|
+
_refIndex;
|
|
26899
|
+
_artifactManager;
|
|
26900
|
+
_consolidationScheduler;
|
|
26901
|
+
_dreamEngine;
|
|
26902
|
+
_llmQueryPlanner;
|
|
26903
|
+
_llmSearchExecutor;
|
|
26904
|
+
_semanticForget;
|
|
26905
|
+
constructor(pathOrOptions) {
|
|
26906
|
+
const opts = typeof pathOrOptions === "string" ? { storagePath: pathOrOptions } : pathOrOptions;
|
|
26907
|
+
this.defaultProjectId = opts.defaultProjectId;
|
|
26908
|
+
const validatedPath = validateFilePath(opts.storagePath, void 0, false);
|
|
26909
|
+
const dir = path5.dirname(validatedPath);
|
|
26910
|
+
const basename2 = path5.basename(validatedPath, path5.extname(validatedPath));
|
|
26911
|
+
this.savedSearchesFilePath = path5.join(dir, `${basename2}-saved-searches.jsonl`);
|
|
26912
|
+
this.tagAliasesFilePath = path5.join(dir, `${basename2}-tag-aliases.jsonl`);
|
|
26913
|
+
this.refIndexFilePath = path5.join(dir, `${basename2}-ref-index.jsonl`);
|
|
26914
|
+
this.storage = createStorageFromPath(validatedPath);
|
|
26915
|
+
if (opts.enableContradictionDetection) {
|
|
26916
|
+
this.initContradictionDetection(opts.contradictionThreshold);
|
|
26917
|
+
}
|
|
26918
|
+
this.observationManager.setMemoryValidator(() => this.memoryValidator);
|
|
26919
|
+
}
|
|
26920
|
+
/**
|
|
26921
|
+
* Wire ContradictionDetector to ObservationManager if a semantic search
|
|
26922
|
+
* embedding provider is available. Silently degrades when none is configured.
|
|
26923
|
+
* @internal
|
|
26924
|
+
*/
|
|
26925
|
+
initContradictionDetection(threshold) {
|
|
26926
|
+
try {
|
|
26927
|
+
const ss = this.semanticSearch;
|
|
26928
|
+
if (!ss) {
|
|
26929
|
+
console.warn(
|
|
26930
|
+
"[ManagerContext] Contradiction detection requested but no embedding provider is configured. Set MEMORY_EMBEDDING_PROVIDER to enable it."
|
|
26931
|
+
);
|
|
26932
|
+
return;
|
|
26933
|
+
}
|
|
26934
|
+
const detector = new ContradictionDetector(ss, threshold ?? 0.85);
|
|
26935
|
+
this.observationManager.setContradictionDetector(detector, this.entityManager);
|
|
26936
|
+
} catch (err) {
|
|
26937
|
+
console.warn(
|
|
26938
|
+
"[ManagerContext] Could not initialise contradiction detection:",
|
|
26939
|
+
err instanceof Error ? err.message : String(err)
|
|
26940
|
+
);
|
|
26941
|
+
}
|
|
26942
|
+
}
|
|
26943
|
+
// ==================== LAZY ACCESSORS (agent memory + semantic) ====================
|
|
26944
|
+
/** EntityManager - Entity CRUD and tag operations */
|
|
26945
|
+
get entityManager() {
|
|
26946
|
+
return this._entityManager ??= new EntityManager(
|
|
26947
|
+
this.storage,
|
|
26948
|
+
{ defaultProjectId: this.defaultProjectId }
|
|
26949
|
+
);
|
|
26950
|
+
}
|
|
26951
|
+
/** RelationManager - Relation CRUD */
|
|
26952
|
+
get relationManager() {
|
|
26953
|
+
return this._relationManager ??= new RelationManager(this.storage);
|
|
26954
|
+
}
|
|
26955
|
+
/** ObservationManager - Observation CRUD */
|
|
26956
|
+
get observationManager() {
|
|
26957
|
+
return this._observationManager ??= new ObservationManager(this.storage);
|
|
26958
|
+
}
|
|
26959
|
+
/** HierarchyManager - Entity hierarchy operations */
|
|
26960
|
+
get hierarchyManager() {
|
|
26961
|
+
return this._hierarchyManager ??= new HierarchyManager(this.storage);
|
|
26962
|
+
}
|
|
26963
|
+
/** GraphTraversal - Phase 4 Sprint 6-8: Graph traversal algorithms */
|
|
26964
|
+
get graphTraversal() {
|
|
26965
|
+
return this._graphTraversal ??= new GraphTraversal(this.storage);
|
|
26966
|
+
}
|
|
26967
|
+
/** SearchManager - All search operations */
|
|
26968
|
+
get searchManager() {
|
|
26969
|
+
return this._searchManager ??= new SearchManager(this.storage, this.savedSearchesFilePath);
|
|
23856
26970
|
}
|
|
23857
26971
|
/** RankedSearch - TF-IDF/BM25 ranked search */
|
|
23858
26972
|
get rankedSearch() {
|
|
@@ -23918,6 +27032,203 @@ var init_ManagerContext = __esm({
|
|
|
23918
27032
|
}
|
|
23919
27033
|
return this._semanticSearch;
|
|
23920
27034
|
}
|
|
27035
|
+
/**
|
|
27036
|
+
* MemoryEngine — turn-aware conversation memory facade composing over
|
|
27037
|
+
* EpisodicMemoryManager + WorkingMemoryManager + ImportanceScorer.
|
|
27038
|
+
* Lazy: instantiated on first access. Reads MEMORY_ENGINE_* env vars
|
|
27039
|
+
* for dedup thresholds, scan window, and scorer weights.
|
|
27040
|
+
*/
|
|
27041
|
+
get memoryEngine() {
|
|
27042
|
+
if (!this._memoryEngine) {
|
|
27043
|
+
const agent = this.agentMemory();
|
|
27044
|
+
const importanceScorer = new ImportanceScorer({
|
|
27045
|
+
lengthWeight: this.getEnvNumber("MEMORY_ENGINE_LENGTH_WEIGHT", 0.3),
|
|
27046
|
+
keywordWeight: this.getEnvNumber("MEMORY_ENGINE_KEYWORD_WEIGHT", 0.4),
|
|
27047
|
+
overlapWeight: this.getEnvNumber("MEMORY_ENGINE_OVERLAP_WEIGHT", 0.3)
|
|
27048
|
+
});
|
|
27049
|
+
const semanticSearch = this.semanticSearch ?? null;
|
|
27050
|
+
const embeddingService = semanticSearch?.getEmbeddingService() ?? null;
|
|
27051
|
+
this._memoryEngine = new MemoryEngine(
|
|
27052
|
+
this.storage,
|
|
27053
|
+
this.entityManager,
|
|
27054
|
+
agent.episodicMemory,
|
|
27055
|
+
agent.workingMemory,
|
|
27056
|
+
importanceScorer,
|
|
27057
|
+
semanticSearch,
|
|
27058
|
+
embeddingService,
|
|
27059
|
+
{
|
|
27060
|
+
jaccardThreshold: this.getEnvNumber("MEMORY_ENGINE_JACCARD_THRESHOLD", 0.72),
|
|
27061
|
+
prefixOverlapThreshold: this.getEnvNumber("MEMORY_ENGINE_PREFIX_OVERLAP", 0.5),
|
|
27062
|
+
dedupScanWindow: Math.trunc(
|
|
27063
|
+
this.getEnvNumber("MEMORY_ENGINE_DEDUP_SCAN_WINDOW", 200)
|
|
27064
|
+
),
|
|
27065
|
+
maxTurnsPerSession: Math.trunc(
|
|
27066
|
+
this.getEnvNumber("MEMORY_ENGINE_MAX_TURNS_PER_SESSION", 1e3)
|
|
27067
|
+
),
|
|
27068
|
+
semanticDedupEnabled: this.getEnvBool("MEMORY_ENGINE_SEMANTIC_DEDUP", false),
|
|
27069
|
+
semanticThreshold: this.getEnvNumber("MEMORY_ENGINE_SEMANTIC_THRESHOLD", 0.92),
|
|
27070
|
+
recentTurnsForImportance: Math.trunc(
|
|
27071
|
+
this.getEnvNumber("MEMORY_ENGINE_RECENT_TURNS", 10)
|
|
27072
|
+
)
|
|
27073
|
+
}
|
|
27074
|
+
);
|
|
27075
|
+
}
|
|
27076
|
+
return this._memoryEngine;
|
|
27077
|
+
}
|
|
27078
|
+
/**
|
|
27079
|
+
* IMemoryBackend (PRD MEM-04) — agent-memory-flavored backend.
|
|
27080
|
+
*
|
|
27081
|
+
* Selection by `MEMORY_BACKEND` env var:
|
|
27082
|
+
* - `sqlite` (default when storage is SQLite OR var unset on JSONL)
|
|
27083
|
+
* - `in-memory` (ephemeral; no persistence)
|
|
27084
|
+
* - future: `postgres`, `vector` (Phase γ)
|
|
27085
|
+
*
|
|
27086
|
+
* Both adapters wrap `ctx.memoryEngine` + `ctx.decayEngine` so they
|
|
27087
|
+
* inherit the four-tier dedup chain and PRD effective-importance
|
|
27088
|
+
* scoring respectively. Lazy-initialized; cached.
|
|
27089
|
+
*/
|
|
27090
|
+
get memoryBackend() {
|
|
27091
|
+
if (!this._memoryBackend) {
|
|
27092
|
+
const choice = (process.env.MEMORY_BACKEND ?? "sqlite").toLowerCase();
|
|
27093
|
+
switch (choice) {
|
|
27094
|
+
case "in-memory":
|
|
27095
|
+
case "inmemory":
|
|
27096
|
+
case "memory":
|
|
27097
|
+
this._memoryBackend = new InMemoryBackend(this.decayEngine);
|
|
27098
|
+
break;
|
|
27099
|
+
case "sqlite":
|
|
27100
|
+
default:
|
|
27101
|
+
this._memoryBackend = new SQLiteBackend(this.memoryEngine, this.decayEngine);
|
|
27102
|
+
break;
|
|
27103
|
+
}
|
|
27104
|
+
}
|
|
27105
|
+
return this._memoryBackend;
|
|
27106
|
+
}
|
|
27107
|
+
/**
|
|
27108
|
+
* MemoryValidator (ROADMAP §3B.1, Phase δ.1) — reflection-stage
|
|
27109
|
+
* service that prevents hallucinations and logical errors from
|
|
27110
|
+
* contaminating memory through self-critique before storage.
|
|
27111
|
+
* Wraps `ContradictionDetector`. Lazy-initialized.
|
|
27112
|
+
*
|
|
27113
|
+
* Construction needs `ContradictionDetector(SemanticSearch, threshold)`.
|
|
27114
|
+
* If no semantic-search backend is configured, a no-op detector is
|
|
27115
|
+
* synthesized so MemoryValidator's other methods still work — the
|
|
27116
|
+
* detection method just returns no contradictions.
|
|
27117
|
+
*/
|
|
27118
|
+
get memoryValidator() {
|
|
27119
|
+
if (!this._memoryValidator) {
|
|
27120
|
+
const ss = this.semanticSearch;
|
|
27121
|
+
const detector = ss ? new ContradictionDetector(ss, 0.85) : new ContradictionDetector(
|
|
27122
|
+
{ calculateSimilarity: async () => 0 },
|
|
27123
|
+
0.85
|
|
27124
|
+
);
|
|
27125
|
+
this._memoryValidator = new MemoryValidator(detector);
|
|
27126
|
+
}
|
|
27127
|
+
return this._memoryValidator;
|
|
27128
|
+
}
|
|
27129
|
+
/**
|
|
27130
|
+
* TrajectoryCompressor (ROADMAP §3B.2, Phase δ.2) — reflection-stage
|
|
27131
|
+
* service that distills verbose interaction histories into compact,
|
|
27132
|
+
* reusable representations. Wraps `compressForContext`. Lazy.
|
|
27133
|
+
*/
|
|
27134
|
+
get trajectoryCompressor() {
|
|
27135
|
+
if (!this._trajectoryCompressor) {
|
|
27136
|
+
this._trajectoryCompressor = new TrajectoryCompressor(this.contextWindowManager);
|
|
27137
|
+
}
|
|
27138
|
+
return this._trajectoryCompressor;
|
|
27139
|
+
}
|
|
27140
|
+
/**
|
|
27141
|
+
* ExperienceExtractor (ROADMAP §3B.3, Phase δ.3) — experience-stage
|
|
27142
|
+
* service that abstracts universal patterns from trajectory clusters
|
|
27143
|
+
* for zero-shot transfer. Wraps `PatternDetector`. Lazy.
|
|
27144
|
+
*/
|
|
27145
|
+
get experienceExtractor() {
|
|
27146
|
+
if (!this._experienceExtractor) {
|
|
27147
|
+
this._experienceExtractor = new ExperienceExtractor(this.patternDetector);
|
|
27148
|
+
}
|
|
27149
|
+
return this._experienceExtractor;
|
|
27150
|
+
}
|
|
27151
|
+
/** Lazy `PatternDetector` instance — backs `experienceExtractor`
|
|
27152
|
+
* but also exposed directly for callers that want pattern detection
|
|
27153
|
+
* without the full Experience-stage wrapper. */
|
|
27154
|
+
get patternDetector() {
|
|
27155
|
+
if (!this._patternDetector) {
|
|
27156
|
+
this._patternDetector = new PatternDetector();
|
|
27157
|
+
}
|
|
27158
|
+
return this._patternDetector;
|
|
27159
|
+
}
|
|
27160
|
+
/**
|
|
27161
|
+
* `ProcedureManager` (3B.4) — first-class executable procedures.
|
|
27162
|
+
* Lazy. Composes `EntityManager` (persists procedures as
|
|
27163
|
+
* `entityType: 'procedure'`).
|
|
27164
|
+
*/
|
|
27165
|
+
get procedureManager() {
|
|
27166
|
+
if (!this._procedureManager) {
|
|
27167
|
+
this._procedureManager = new ProcedureManager(this.entityManager);
|
|
27168
|
+
}
|
|
27169
|
+
return this._procedureManager;
|
|
27170
|
+
}
|
|
27171
|
+
/**
|
|
27172
|
+
* `CausalReasoner` (3B.6) — symbolic forward / backward / counter-
|
|
27173
|
+
* factual inference over the causal subgraph. Wraps `GraphTraversal`.
|
|
27174
|
+
* Lazy.
|
|
27175
|
+
*/
|
|
27176
|
+
get causalReasoner() {
|
|
27177
|
+
if (!this._causalReasoner) {
|
|
27178
|
+
this._causalReasoner = new CausalReasoner(this.graphTraversal);
|
|
27179
|
+
}
|
|
27180
|
+
return this._causalReasoner;
|
|
27181
|
+
}
|
|
27182
|
+
/**
|
|
27183
|
+
* `RoleAssignmentStore` (η.6.1) — registry of role grants per agent.
|
|
27184
|
+
* Lazy. In-memory only by default; configure persistence via the
|
|
27185
|
+
* direct constructor if a JSONL sidecar is wanted.
|
|
27186
|
+
*/
|
|
27187
|
+
get roleAssignmentStore() {
|
|
27188
|
+
if (!this._roleAssignmentStore) {
|
|
27189
|
+
this._roleAssignmentStore = new RoleAssignmentStore();
|
|
27190
|
+
}
|
|
27191
|
+
return this._roleAssignmentStore;
|
|
27192
|
+
}
|
|
27193
|
+
/**
|
|
27194
|
+
* `RbacMiddleware` (η.6.1) — `RbacPolicy` impl. Backed by the lazy
|
|
27195
|
+
* `roleAssignmentStore`. Lazy.
|
|
27196
|
+
*/
|
|
27197
|
+
get rbacMiddleware() {
|
|
27198
|
+
if (!this._rbacMiddleware) {
|
|
27199
|
+
this._rbacMiddleware = new RbacMiddleware(this.roleAssignmentStore);
|
|
27200
|
+
}
|
|
27201
|
+
return this._rbacMiddleware;
|
|
27202
|
+
}
|
|
27203
|
+
/**
|
|
27204
|
+
* `WorldModelManager` (3B.7) — orchestrator that composes
|
|
27205
|
+
* `causalReasoner`, `memoryValidator`, and `entityManager` into a
|
|
27206
|
+
* single facade for `getCurrentState` / `validateFact` /
|
|
27207
|
+
* `predictOutcome` / `detectStateChange`. Lazy. Causal and validator
|
|
27208
|
+
* dependencies are passed through; methods that need them gracefully
|
|
27209
|
+
* return null/empty when they're not configured.
|
|
27210
|
+
*/
|
|
27211
|
+
get worldModelManager() {
|
|
27212
|
+
if (!this._worldModelManager) {
|
|
27213
|
+
this._worldModelManager = new WorldModelManager(
|
|
27214
|
+
this.entityManager,
|
|
27215
|
+
this.causalReasoner,
|
|
27216
|
+
this.memoryValidator
|
|
27217
|
+
);
|
|
27218
|
+
}
|
|
27219
|
+
return this._worldModelManager;
|
|
27220
|
+
}
|
|
27221
|
+
/**
|
|
27222
|
+
* `ActiveRetrievalController` (3B.5) — adaptive query-rewriting loop
|
|
27223
|
+
* over `RankedSearch`. Lazy. Pure symbolic expansion (no LLM); for
|
|
27224
|
+
* LLM-driven decomposition use `ctx.llmQueryPlanner`.
|
|
27225
|
+
*/
|
|
27226
|
+
get activeRetrieval() {
|
|
27227
|
+
if (!this._activeRetrieval) {
|
|
27228
|
+
this._activeRetrieval = new ActiveRetrievalController(this.rankedSearch);
|
|
27229
|
+
}
|
|
27230
|
+
return this._activeRetrieval;
|
|
27231
|
+
}
|
|
23921
27232
|
/**
|
|
23922
27233
|
* TransitionLedger - Append-only audit trail for state changes.
|
|
23923
27234
|
* Returns null if not enabled via MEMORY_TRANSITION_LEDGER env var.
|
|
@@ -23980,11 +27291,25 @@ var init_ManagerContext = __esm({
|
|
|
23980
27291
|
halfLifeHours: this.getEnvNumber("MEMORY_DECAY_HALF_LIFE_HOURS", 168),
|
|
23981
27292
|
minImportance: this.getEnvNumber("MEMORY_DECAY_MIN_IMPORTANCE", 0.1),
|
|
23982
27293
|
importanceModulation: this.getEnvBool("MEMORY_DECAY_IMPORTANCE_MOD", true),
|
|
23983
|
-
accessModulation: this.getEnvBool("MEMORY_DECAY_ACCESS_MOD", true)
|
|
27294
|
+
accessModulation: this.getEnvBool("MEMORY_DECAY_ACCESS_MOD", true),
|
|
27295
|
+
// PRD MEM-01 (v1.12.0). decayRate is auto-derived from halfLifeHours
|
|
27296
|
+
// when env-var unset (NaN check avoids overriding the auto-derive).
|
|
27297
|
+
decayRate: this.envNumberOrUndefined("MEMORY_PRD_DECAY_RATE"),
|
|
27298
|
+
freshnessCoefficient: this.getEnvNumber("MEMORY_PRD_FRESHNESS_COEFFICIENT", 0.01),
|
|
27299
|
+
relevanceWeight: this.getEnvNumber("MEMORY_PRD_RELEVANCE_WEIGHT", 0.35),
|
|
27300
|
+
minImportanceThreshold: this.getEnvNumber("MEMORY_PRD_MIN_IMPORTANCE_THRESHOLD", 0.1)
|
|
23984
27301
|
});
|
|
23985
27302
|
}
|
|
23986
27303
|
return this._decayEngine;
|
|
23987
27304
|
}
|
|
27305
|
+
/** Returns env-var as number, or undefined when unset (lets the
|
|
27306
|
+
* default-derive logic in DecayEngine kick in for `decayRate`). */
|
|
27307
|
+
envNumberOrUndefined(name) {
|
|
27308
|
+
const raw = process.env[name];
|
|
27309
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
27310
|
+
const n = Number(raw);
|
|
27311
|
+
return Number.isFinite(n) ? n : void 0;
|
|
27312
|
+
}
|
|
23988
27313
|
/**
|
|
23989
27314
|
* DecayScheduler - Scheduled decay and forget operations.
|
|
23990
27315
|
* Returns undefined if auto-decay is not enabled (MEMORY_AUTO_DECAY).
|
|
@@ -24145,6 +27470,10 @@ var init_ManagerContext = __esm({
|
|
|
24145
27470
|
agentMemory(config) {
|
|
24146
27471
|
if (!this._agentMemory || config) {
|
|
24147
27472
|
this._agentMemory = new AgentMemoryManager(this.storage, config);
|
|
27473
|
+
this._memoryEngine = void 0;
|
|
27474
|
+
this._memoryBackend = void 0;
|
|
27475
|
+
this._consolidationScheduler = void 0;
|
|
27476
|
+
this._dreamEngine = void 0;
|
|
24148
27477
|
}
|
|
24149
27478
|
return this._agentMemory;
|
|
24150
27479
|
}
|
|
@@ -24427,7 +27756,7 @@ Path (${pathResult.length} hops): ${pathResult.path.join(" -> ")}`);
|
|
|
24427
27756
|
break;
|
|
24428
27757
|
}
|
|
24429
27758
|
case "export": {
|
|
24430
|
-
const validFormats = ["json", "csv", "graphml", "gexf", "dot", "markdown", "mermaid"];
|
|
27759
|
+
const validFormats = ["json", "csv", "graphml", "gexf", "dot", "markdown", "mermaid", "turtle", "rdf-xml", "json-ld"];
|
|
24431
27760
|
const fmt = args[0] || "json";
|
|
24432
27761
|
if (!validFormats.includes(fmt)) {
|
|
24433
27762
|
console.log(chalk2.yellow(`Invalid format: ${fmt}. Use: ${validFormats.join(", ")}`));
|
|
@@ -24501,14 +27830,11 @@ var defaultOptions = {
|
|
|
24501
27830
|
verbose: false
|
|
24502
27831
|
};
|
|
24503
27832
|
function parseGlobalOptions(opts) {
|
|
24504
|
-
const
|
|
24505
|
-
|
|
24506
|
-
console.error(`Invalid format: ${format}. Use json, table, or csv.`);
|
|
24507
|
-
process.exit(1);
|
|
24508
|
-
}
|
|
27833
|
+
const rawFormat = opts.outputFormat;
|
|
27834
|
+
const format = rawFormat && ["json", "table", "csv"].includes(rawFormat) ? rawFormat : defaultOptions.format;
|
|
24509
27835
|
return {
|
|
24510
27836
|
storage: opts.storage || defaultOptions.storage,
|
|
24511
|
-
format
|
|
27837
|
+
format,
|
|
24512
27838
|
quiet: Boolean(opts.quiet),
|
|
24513
27839
|
verbose: Boolean(opts.verbose)
|
|
24514
27840
|
};
|
|
@@ -25365,8 +28691,9 @@ init_esm_shims();
|
|
|
25365
28691
|
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
25366
28692
|
import { resolve as resolve2 } from "path";
|
|
25367
28693
|
import { Option } from "commander";
|
|
28694
|
+
init_entityUtils();
|
|
25368
28695
|
var IMPORT_FORMATS = ["json", "csv", "graphml"];
|
|
25369
|
-
var EXPORT_FORMATS = ["json", "csv", "graphml", "gexf", "dot", "markdown", "mermaid"];
|
|
28696
|
+
var EXPORT_FORMATS = ["json", "csv", "graphml", "gexf", "dot", "markdown", "mermaid", "turtle", "rdf-xml", "json-ld"];
|
|
25370
28697
|
function registerIOCommands(program2) {
|
|
25371
28698
|
program2.command("import <file>").description("Import data from file").addOption(
|
|
25372
28699
|
new Option("-f, --format <format>", "File format").choices([...IMPORT_FORMATS]).default("json")
|
|
@@ -25375,7 +28702,7 @@ function registerIOCommands(program2) {
|
|
|
25375
28702
|
const logger2 = createLogger(options);
|
|
25376
28703
|
const ctx = createContext(options);
|
|
25377
28704
|
try {
|
|
25378
|
-
const resolvedPath = resolve2(file);
|
|
28705
|
+
const resolvedPath = validateFilePath(resolve2(file), void 0, false);
|
|
25379
28706
|
const data = readFileSync2(resolvedPath, "utf-8");
|
|
25380
28707
|
const result = await ctx.ioManager.importGraph(
|
|
25381
28708
|
opts.format,
|
|
@@ -25397,7 +28724,7 @@ function registerIOCommands(program2) {
|
|
|
25397
28724
|
const logger2 = createLogger(options);
|
|
25398
28725
|
const ctx = createContext(options);
|
|
25399
28726
|
try {
|
|
25400
|
-
const resolvedPath = resolve2(file);
|
|
28727
|
+
const resolvedPath = validateFilePath(resolve2(file), void 0, false);
|
|
25401
28728
|
const graph = await ctx.storage.loadGraph();
|
|
25402
28729
|
const data = ctx.ioManager.exportGraph(
|
|
25403
28730
|
graph,
|
|
@@ -25568,7 +28895,7 @@ function getVersion() {
|
|
|
25568
28895
|
}
|
|
25569
28896
|
var program = new Command2();
|
|
25570
28897
|
program.name("memory").description("MemoryJS - Knowledge Graph CLI").version(getVersion(), "-v, --version", "Output the current version");
|
|
25571
|
-
program.option("-s, --storage <path>", "Path to storage file", "./memory.jsonl").option("-
|
|
28898
|
+
program.option("-s, --storage <path>", "Path to storage file", "./memory.jsonl").option("--output-format <type>", "Console output format (json|table|csv)", "json").option("-q, --quiet", "Suppress non-essential output").option("--verbose", "Enable verbose/debug output");
|
|
25572
28899
|
registerCommands(program);
|
|
25573
28900
|
program.parse();
|
|
25574
28901
|
//# sourceMappingURL=index.js.map
|