@elizaos/plugin-experience 2.0.0-alpha.2 → 2.0.0-alpha.4

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.
@@ -368,7 +368,7 @@ var experienceEvaluator = {
368
368
  const messageCount = Number.parseInt(currentCount, 10);
369
369
  const newMessageCount = messageCount + 1;
370
370
  await runtime.setCache(lastExtractionKey, newMessageCount.toString());
371
- const shouldExtract = newMessageCount % 10 === 0;
371
+ const shouldExtract = newMessageCount % 25 === 0;
372
372
  if (shouldExtract) {
373
373
  logger2.info(`[experienceEvaluator] Triggering experience extraction after ${newMessageCount} messages`);
374
374
  }
@@ -391,21 +391,14 @@ var experienceEvaluator = {
391
391
  return;
392
392
  }
393
393
  const conversationContext = recentMessages.map((m) => m.content.text).filter(Boolean).join(" ");
394
- const existingExperiences = await experienceService.queryExperiences({
395
- query: conversationContext,
396
- limit: 10,
397
- minConfidence: 0.7
398
- });
399
- const existingExperiencesText = existingExperiences.length > 0 ? existingExperiences.map((exp) => `- ${exp.learning}`).join(`
400
- `) : "None";
401
394
  const extractionPrompt = composePrompt({
402
395
  state: {
403
396
  conversation_context: conversationContext,
404
- existing_experiences: existingExperiencesText
397
+ existing_experiences: "None"
405
398
  },
406
399
  template: EXTRACT_EXPERIENCES_TEMPLATE
407
400
  });
408
- const response = await runtime.useModel(ModelType.TEXT_LARGE, {
401
+ const response = await runtime.useModel(ModelType.TEXT_SMALL, {
409
402
  prompt: extractionPrompt
410
403
  });
411
404
  const experiences = parseExtractedExperiences(response);
@@ -420,6 +413,19 @@ var experienceEvaluator = {
420
413
  if (!exp.learning || typeof exp.confidence !== "number" || exp.confidence < threshold) {
421
414
  continue;
422
415
  }
416
+ const similar = await experienceService.findSimilarExperiences(exp.learning, 1);
417
+ if (similar.length > 0) {
418
+ const existingLearning = similar[0].learning.toLowerCase();
419
+ const newLearning = exp.learning.toLowerCase();
420
+ const existingWords = new Set(existingLearning.split(/\s+/).filter((w) => w.length > 3));
421
+ const newWords = new Set(newLearning.split(/\s+/).filter((w) => w.length > 3));
422
+ const overlap = [...newWords].filter((w) => existingWords.has(w)).length;
423
+ const union = new Set([...existingWords, ...newWords]).size;
424
+ if (union > 0 && overlap / union > 0.6) {
425
+ logger2.debug(`[experienceEvaluator] Skipping duplicate experience: "${exp.learning.substring(0, 80)}..."`);
426
+ continue;
427
+ }
428
+ }
423
429
  const normalizedType = typeof exp.type === "string" ? exp.type.toUpperCase() : "";
424
430
  const experienceType = experienceTypeMap[normalizedType] ?? "learning" /* LEARNING */;
425
431
  const experienceTag = experienceType;
@@ -756,35 +762,28 @@ class ExperienceService extends Service {
756
762
  experiences = new Map;
757
763
  experiencesByDomain = new Map;
758
764
  experiencesByType = new Map;
759
- maxExperiences = 1e4;
765
+ dirtyExperiences = new Set;
766
+ persistTimer = null;
760
767
  decayManager;
761
768
  relationshipManager;
762
769
  constructor(runtime) {
763
770
  super(runtime);
764
771
  this.decayManager = new ConfidenceDecayManager;
765
772
  this.relationshipManager = new ExperienceRelationshipManager;
766
- const max = getNumberSetting2(runtime, "MAX_EXPERIENCES", this.maxExperiences);
767
- if (Number.isFinite(max) && max > 0) {
768
- this.maxExperiences = Math.floor(max);
769
- }
770
773
  this.loadExperiences();
774
+ this.persistTimer = setInterval(() => {
775
+ this.persistDirtyExperiences();
776
+ }, 60000);
771
777
  }
772
778
  static async start(runtime) {
773
779
  const service = new ExperienceService(runtime);
774
780
  return service;
775
781
  }
776
- setMaxExperiences(maxExperiences) {
777
- if (!Number.isFinite(maxExperiences) || maxExperiences <= 0)
778
- return;
779
- this.maxExperiences = Math.floor(maxExperiences);
780
- }
781
782
  async loadExperiences() {
782
- const allMemories = await this.runtime.getMemories({
783
+ const memories = await this.runtime.getMemories({
783
784
  entityId: this.runtime.agentId,
784
- count: this.maxExperiences,
785
- tableName: "memories"
785
+ tableName: "experiences"
786
786
  });
787
- const memories = allMemories.filter((m) => m.content.type === "experience");
788
787
  for (const memory of memories) {
789
788
  const experienceData = memory.content.data;
790
789
  if (experienceData?.id) {
@@ -836,9 +835,19 @@ class ExperienceService extends Service {
836
835
  }
837
836
  async recordExperience(experienceData) {
838
837
  const embeddingText = `${experienceData.context} ${experienceData.action} ${experienceData.result} ${experienceData.learning}`;
839
- const embedding = await this.runtime.useModel(ModelType2.TEXT_EMBEDDING, {
840
- text: embeddingText
841
- });
838
+ let embedding;
839
+ try {
840
+ const result = await this.runtime.useModel(ModelType2.TEXT_EMBEDDING, {
841
+ text: embeddingText
842
+ });
843
+ if (Array.isArray(result) && result.length > 0 && result.some((v) => v !== 0)) {
844
+ embedding = result;
845
+ } else {
846
+ logger4.warn("[ExperienceService] Embedding model returned empty/zero vector, storing without embedding");
847
+ }
848
+ } catch (err) {
849
+ logger4.warn(`[ExperienceService] Embedding generation failed, storing without embedding: ${err}`);
850
+ }
842
851
  const now = Date.now();
843
852
  const experience = {
844
853
  id: uuidv4(),
@@ -883,9 +892,6 @@ class ExperienceService extends Service {
883
892
  strength: 0.8
884
893
  });
885
894
  }
886
- if (this.experiences.size > this.maxExperiences) {
887
- await this.pruneOldExperiences();
888
- }
889
895
  logger4.info(`[ExperienceService] Recorded experience: ${experience.id} (${experience.type})`);
890
896
  return experience;
891
897
  }
@@ -915,7 +921,6 @@ class ExperienceService extends Service {
915
921
  updatedAt: experience.updatedAt,
916
922
  accessCount: experience.accessCount,
917
923
  lastAccessedAt: experience.lastAccessedAt,
918
- embedding: experience.embedding,
919
924
  relatedExperiences: experience.relatedExperiences,
920
925
  supersedes: experience.supersedes,
921
926
  previousBelief: experience.previousBelief,
@@ -926,88 +931,43 @@ class ExperienceService extends Service {
926
931
  };
927
932
  await this.runtime.createMemory(memory, "experiences", true);
928
933
  }
934
+ async persistDirtyExperiences() {
935
+ if (this.dirtyExperiences.size === 0)
936
+ return;
937
+ const toSave = Array.from(this.dirtyExperiences);
938
+ this.dirtyExperiences.clear();
939
+ let saved = 0;
940
+ for (const id of toSave) {
941
+ const exp = this.experiences.get(id);
942
+ if (exp) {
943
+ try {
944
+ await this.saveExperienceToMemory(exp);
945
+ saved++;
946
+ } catch {
947
+ this.dirtyExperiences.add(id);
948
+ }
949
+ }
950
+ }
951
+ if (saved > 0) {
952
+ logger4.debug(`[ExperienceService] Persisted ${saved} dirty experiences`);
953
+ }
954
+ }
929
955
  async queryExperiences(query) {
930
956
  let results = [];
957
+ const limit = query.limit || 10;
931
958
  if (query.query) {
932
- const similarExperiences = await this.findSimilarExperiences(query.query, query.limit || 10);
933
- let candidates = similarExperiences;
934
- if (query.type) {
935
- const types = Array.isArray(query.type) ? query.type : [query.type];
936
- candidates = candidates.filter((exp) => types.includes(exp.type));
937
- }
938
- if (query.outcome) {
939
- candidates = candidates.filter((exp) => exp.outcome === query.outcome);
940
- }
941
- if (query.domain) {
942
- const domains = Array.isArray(query.domain) ? query.domain : [query.domain];
943
- candidates = candidates.filter((exp) => domains.includes(exp.domain));
944
- }
945
- if (query.tags && query.tags.length > 0) {
946
- candidates = candidates.filter((exp) => query.tags?.some((tag) => exp.tags.includes(tag)));
947
- }
948
- if (query.minConfidence !== undefined) {
949
- const minConfidence = query.minConfidence;
950
- candidates = candidates.filter((exp) => {
951
- const decayedConfidence = this.decayManager.getDecayedConfidence(exp);
952
- return decayedConfidence >= minConfidence;
953
- });
954
- }
955
- if (query.minImportance !== undefined) {
956
- const minImportance = query.minImportance;
957
- candidates = candidates.filter((exp) => exp.importance >= minImportance);
958
- }
959
- if (query.timeRange) {
960
- candidates = candidates.filter((exp) => {
961
- if (query.timeRange?.start && exp.createdAt < query.timeRange?.start)
962
- return false;
963
- if (query.timeRange?.end && exp.createdAt > query.timeRange?.end)
964
- return false;
965
- return true;
966
- });
967
- }
968
- results = candidates;
959
+ const hasFilters = !!(query.type || query.outcome || query.domain || query.tags && query.tags.length > 0 || query.minConfidence !== undefined || query.minImportance !== undefined || query.timeRange);
960
+ const fetchLimit = hasFilters ? Math.max(limit * 5, 50) : limit;
961
+ const candidates = this.applyFilters(await this.findSimilarExperiences(query.query, fetchLimit), query);
962
+ results = candidates.slice(0, limit);
969
963
  } else {
970
- let candidates = Array.from(this.experiences.values());
971
- if (query.type) {
972
- const types = Array.isArray(query.type) ? query.type : [query.type];
973
- candidates = candidates.filter((exp) => types.includes(exp.type));
974
- }
975
- if (query.outcome) {
976
- candidates = candidates.filter((exp) => exp.outcome === query.outcome);
977
- }
978
- if (query.domain) {
979
- const domains = Array.isArray(query.domain) ? query.domain : [query.domain];
980
- candidates = candidates.filter((exp) => domains.includes(exp.domain));
981
- }
982
- if (query.tags && query.tags.length > 0) {
983
- candidates = candidates.filter((exp) => query.tags?.some((tag) => exp.tags.includes(tag)));
984
- }
985
- if (query.minConfidence !== undefined) {
986
- const minConfidence = query.minConfidence;
987
- candidates = candidates.filter((exp) => {
988
- const decayedConfidence = this.decayManager.getDecayedConfidence(exp);
989
- return decayedConfidence >= minConfidence;
990
- });
991
- }
992
- if (query.minImportance !== undefined) {
993
- const minImportance = query.minImportance;
994
- candidates = candidates.filter((exp) => exp.importance >= minImportance);
995
- }
996
- if (query.timeRange) {
997
- candidates = candidates.filter((exp) => {
998
- if (query.timeRange?.start && exp.createdAt < query.timeRange?.start)
999
- return false;
1000
- if (query.timeRange?.end && exp.createdAt > query.timeRange?.end)
1001
- return false;
1002
- return true;
1003
- });
1004
- }
964
+ const candidates = this.applyFilters(Array.from(this.experiences.values()), query);
1005
965
  candidates.sort((a, b) => {
1006
966
  const scoreA = this.decayManager.getDecayedConfidence(a) * a.importance;
1007
967
  const scoreB = this.decayManager.getDecayedConfidence(b) * b.importance;
1008
968
  return scoreB - scoreA;
1009
969
  });
1010
- results = candidates.slice(0, query.limit || 10);
970
+ results = candidates.slice(0, limit);
1011
971
  }
1012
972
  if (query.includeRelated) {
1013
973
  const relatedIds = new Set;
@@ -1024,31 +984,96 @@ class ExperienceService extends Service {
1024
984
  for (const exp of results) {
1025
985
  exp.accessCount++;
1026
986
  exp.lastAccessedAt = Date.now();
987
+ this.dirtyExperiences.add(exp.id);
1027
988
  }
1028
989
  return results;
1029
990
  }
991
+ applyFilters(candidates, query) {
992
+ let filtered = candidates;
993
+ if (query.type) {
994
+ const types = Array.isArray(query.type) ? query.type : [query.type];
995
+ filtered = filtered.filter((e) => types.includes(e.type));
996
+ }
997
+ if (query.outcome) {
998
+ filtered = filtered.filter((e) => e.outcome === query.outcome);
999
+ }
1000
+ if (query.domain) {
1001
+ const domains = Array.isArray(query.domain) ? query.domain : [query.domain];
1002
+ filtered = filtered.filter((e) => domains.includes(e.domain));
1003
+ }
1004
+ if (query.tags && query.tags.length > 0) {
1005
+ filtered = filtered.filter((e) => query.tags?.some((t) => e.tags.includes(t)));
1006
+ }
1007
+ if (query.minConfidence !== undefined) {
1008
+ const min = query.minConfidence;
1009
+ filtered = filtered.filter((e) => this.decayManager.getDecayedConfidence(e) >= min);
1010
+ }
1011
+ if (query.minImportance !== undefined) {
1012
+ const min = query.minImportance;
1013
+ filtered = filtered.filter((e) => e.importance >= min);
1014
+ }
1015
+ if (query.timeRange) {
1016
+ const { start, end } = query.timeRange;
1017
+ filtered = filtered.filter((e) => {
1018
+ if (start && e.createdAt < start)
1019
+ return false;
1020
+ if (end && e.createdAt > end)
1021
+ return false;
1022
+ return true;
1023
+ });
1024
+ }
1025
+ return filtered;
1026
+ }
1030
1027
  async findSimilarExperiences(text, limit = 5) {
1031
1028
  if (!text || this.experiences.size === 0) {
1032
1029
  return [];
1033
1030
  }
1034
- const queryEmbedding = await this.runtime.useModel(ModelType2.TEXT_EMBEDDING, {
1035
- text
1036
- });
1037
- const similarities = [];
1038
- for (const experience of this.experiences.values()) {
1039
- if (experience.embedding) {
1040
- const similarity = this.cosineSimilarity(queryEmbedding, experience.embedding);
1041
- similarities.push({ experience, similarity });
1031
+ let queryEmbedding;
1032
+ try {
1033
+ queryEmbedding = await this.runtime.useModel(ModelType2.TEXT_EMBEDDING, { text });
1034
+ if (!Array.isArray(queryEmbedding) || queryEmbedding.length === 0 || queryEmbedding.every((v) => v === 0)) {
1035
+ logger4.warn("[ExperienceService] Query embedding is empty/zero, falling back to recency sort");
1036
+ return this.fallbackSort(limit);
1042
1037
  }
1038
+ } catch {
1039
+ logger4.warn("[ExperienceService] Query embedding failed, falling back to recency sort");
1040
+ return this.fallbackSort(limit);
1043
1041
  }
1044
- similarities.sort((a, b) => b.similarity - a.similarity);
1045
- const results = similarities.slice(0, limit).map((item) => item.experience);
1042
+ const SIMILARITY_FLOOR = 0.05;
1043
+ const scored = [];
1044
+ const now = Date.now();
1045
+ for (const experience of this.experiences.values()) {
1046
+ if (!experience.embedding)
1047
+ continue;
1048
+ const similarity = this.cosineSimilarity(queryEmbedding, experience.embedding);
1049
+ if (similarity < SIMILARITY_FLOOR)
1050
+ continue;
1051
+ const decayedConfidence = this.decayManager.getDecayedConfidence(experience);
1052
+ const ageDays = Math.max(0, (now - experience.createdAt) / (24 * 60 * 60 * 1000));
1053
+ const recencyFactor = 1 / (1 + ageDays / 30);
1054
+ const accessFactor = Math.min(1, Math.log2(experience.accessCount + 1) / Math.log2(10));
1055
+ const qualityScore = decayedConfidence * 0.45 + experience.importance * 0.35 + recencyFactor * 0.12 + accessFactor * 0.08;
1056
+ const rerankScore = similarity * 0.7 + qualityScore * 0.3;
1057
+ scored.push({ experience, score: rerankScore });
1058
+ }
1059
+ scored.sort((a, b) => b.score - a.score);
1060
+ const results = scored.slice(0, limit).map((item) => item.experience);
1046
1061
  for (const exp of results) {
1047
1062
  exp.accessCount++;
1048
- exp.lastAccessedAt = Date.now();
1063
+ exp.lastAccessedAt = now;
1064
+ this.dirtyExperiences.add(exp.id);
1049
1065
  }
1050
1066
  return results;
1051
1067
  }
1068
+ fallbackSort(limit) {
1069
+ const all = Array.from(this.experiences.values());
1070
+ all.sort((a, b) => {
1071
+ const sa = this.decayManager.getDecayedConfidence(a) * a.importance;
1072
+ const sb = this.decayManager.getDecayedConfidence(b) * b.importance;
1073
+ return sb - sa;
1074
+ });
1075
+ return all.slice(0, limit);
1076
+ }
1052
1077
  async analyzeExperiences(domain, type) {
1053
1078
  const experiences = await this.queryExperiences({
1054
1079
  domain: domain ? [domain] : undefined,
@@ -1176,63 +1201,26 @@ class ExperienceService extends Service {
1176
1201
  }
1177
1202
  return recommendations.slice(0, 5);
1178
1203
  }
1179
- async pruneOldExperiences() {
1180
- if (this.experiences.size <= this.maxExperiences) {
1181
- return;
1182
- }
1183
- const experienceArray = Array.from(this.experiences.values());
1184
- experienceArray.sort((a, b) => {
1185
- if (a.importance !== b.importance) {
1186
- return a.importance - b.importance;
1187
- }
1188
- if (a.accessCount !== b.accessCount) {
1189
- return a.accessCount - b.accessCount;
1190
- }
1191
- return a.createdAt - b.createdAt;
1192
- });
1193
- const toRemove = experienceArray.slice(0, experienceArray.length - this.maxExperiences);
1194
- let removedCount = 0;
1195
- for (const experience of toRemove) {
1196
- this.experiences.delete(experience.id);
1197
- const domainSet = this.experiencesByDomain.get(experience.domain);
1198
- if (domainSet) {
1199
- domainSet.delete(experience.id);
1200
- if (domainSet.size === 0) {
1201
- this.experiencesByDomain.delete(experience.domain);
1202
- }
1203
- }
1204
- const typeSet = this.experiencesByType.get(experience.type);
1205
- if (typeSet) {
1206
- typeSet.delete(experience.id);
1207
- if (typeSet.size === 0) {
1208
- this.experiencesByType.delete(experience.type);
1209
- }
1210
- }
1211
- removedCount++;
1212
- }
1213
- logger4.info(`[ExperienceService] Pruned ${removedCount} old experiences`);
1214
- }
1215
1204
  async stop() {
1216
1205
  logger4.info("[ExperienceService] Stopping...");
1206
+ if (this.persistTimer) {
1207
+ clearInterval(this.persistTimer);
1208
+ this.persistTimer = null;
1209
+ }
1217
1210
  const experiencesToSave = Array.from(this.experiences.values());
1218
1211
  let savedCount = 0;
1219
1212
  for (const experience of experiencesToSave) {
1220
- await this.saveExperienceToMemory(experience);
1221
- savedCount++;
1213
+ try {
1214
+ await this.saveExperienceToMemory(experience);
1215
+ savedCount++;
1216
+ } catch (err) {
1217
+ logger4.warn(`[ExperienceService] Failed to save experience ${experience.id}: ${err}`);
1218
+ }
1222
1219
  }
1220
+ this.dirtyExperiences.clear();
1223
1221
  logger4.info(`[ExperienceService] Saved ${savedCount} experiences`);
1224
1222
  }
1225
1223
  }
1226
- function getNumberSetting2(runtime, key, fallback) {
1227
- const value = runtime.getSetting(key);
1228
- if (typeof value === "number")
1229
- return value;
1230
- if (typeof value === "string") {
1231
- const parsed = Number.parseFloat(value);
1232
- return Number.isFinite(parsed) ? parsed : fallback;
1233
- }
1234
- return fallback;
1235
- }
1236
1224
 
1237
1225
  // index.ts
1238
1226
  var experiencePlugin = {
@@ -1244,14 +1232,9 @@ var experiencePlugin = {
1244
1232
  evaluators: [experienceEvaluator],
1245
1233
  async init(config, runtime) {
1246
1234
  logger5.info("[ExperiencePlugin] Initializing experience learning system");
1247
- const maxExperiences = parseOptionalNumber(config.MAX_EXPERIENCES, 1e4);
1248
1235
  const autoRecordThreshold = parseOptionalNumber(config.AUTO_RECORD_THRESHOLD, 0.7);
1249
- runtime.setSetting("MAX_EXPERIENCES", maxExperiences.toString());
1250
1236
  runtime.setSetting("AUTO_RECORD_THRESHOLD", autoRecordThreshold.toString());
1251
- const experienceService = runtime.getService("EXPERIENCE");
1252
- experienceService?.setMaxExperiences(maxExperiences);
1253
1237
  logger5.info(`[ExperiencePlugin] Configuration:
1254
- - MAX_EXPERIENCES: ${maxExperiences}
1255
1238
  - AUTO_RECORD_THRESHOLD: ${autoRecordThreshold}`);
1256
1239
  }
1257
1240
  };
@@ -1271,4 +1254,4 @@ export {
1271
1254
  ExperienceService
1272
1255
  };
1273
1256
 
1274
- //# debugId=332CCD4C6A890DF464756E2164756E21
1257
+ //# debugId=19BED8D33D8C8ECD64756E2164756E21