llmemory 0.1.16 → 0.1.17

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 135c27c05f80b660972a5aa8df5ba315cbdd8235ac68c662ddcfe9249ca79109
4
- data.tar.gz: 2e43bacc332ddb44613c79ebd7e4e832091fc68c451a75073860502962d496d9
3
+ metadata.gz: b86647810e47140fff5732a066da4a16188704249d09db14720d8c565b6eaf0e
4
+ data.tar.gz: 7c52c551746d22c29e41015098a68e166788bb614b0e0c2b064fa5fc0824989f
5
5
  SHA512:
6
- metadata.gz: 9b0d7d67c2647ec0392f993ed31aedc5d51fdc3dadf40bd1a511b90a55b24ce415a9b9a420cc6e676d5a3ba8eb780048e047eff9d0800618dc10e99c4c779a1f
7
- data.tar.gz: 10184e66c86c94976d2c164f8717633fbb438e0e95cfb0dbffed4ce52f8bbf8c4266c0b3e48879c792250da15a385464983a886408c69598044c0f60c13f27e7
6
+ metadata.gz: f584d487eca13280b13f7a3e9f4b8eda60ed10ec8dbb45ff72483acbc76b7feb7f79fb5ae572640a31e87ade05b0762f07cacbecb778dffa6d62a7096231fb12
7
+ data.tar.gz: c68e284c21fc22ccdf20160d9082575a47482b03c5a91fb3b52ec5a6c51b7f5cf13a8915bfd3fe2f5933aa6a52fe8c382e2373393b27d68d8ed1c7ac83766e49
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Llmemory
6
+ module LongTerm
7
+ module Episodic
8
+ # An Episode is a trajectory of an agent's experience: an ordered list of
9
+ # steps (observation -> action -> result) plus a summary, an outcome label
10
+ # and an importance score. This is CoALA's "episodic memory" — distinct
11
+ # from semantic memory (facts), it stores what happened so it can later be
12
+ # retrieved as examples or distilled into semantic knowledge (see P2,
13
+ # reflection).
14
+ class Episode
15
+ attr_reader :id, :user_id, :steps, :summary, :outcome, :importance, :provenance, :created_at
16
+
17
+ STEP_KEYS = %i[observation action result timestamp].freeze
18
+
19
+ def initialize(id:, user_id:, steps: [], summary: nil, outcome: nil, importance: 0.5, provenance: nil, created_at: nil)
20
+ @id = id
21
+ @user_id = user_id
22
+ @steps = self.class.normalize_steps(steps)
23
+ @summary = summary
24
+ @outcome = outcome
25
+ @importance = importance.nil? ? 0.5 : importance.to_f
26
+ @provenance = provenance
27
+ @created_at = created_at || Time.now
28
+ end
29
+
30
+ # Flat, searchable representation used for keyword retrieval and, in the
31
+ # future, embedding. Combines summary, outcome and every step field.
32
+ def searchable_text
33
+ parts = [summary, outcome]
34
+ steps.each do |s|
35
+ parts << s[:observation]
36
+ parts << s[:action]
37
+ parts << s[:result]
38
+ end
39
+ parts.compact.map(&:to_s).reject(&:empty?).join("\n")
40
+ end
41
+
42
+ def self.normalize_steps(steps)
43
+ Array(steps).filter_map do |step|
44
+ next nil unless step.is_a?(Hash)
45
+ {
46
+ observation: step[:observation] || step["observation"],
47
+ action: step[:action] || step["action"],
48
+ result: step[:result] || step["result"],
49
+ timestamp: normalize_time(step[:timestamp] || step["timestamp"])
50
+ }
51
+ end
52
+ end
53
+
54
+ def self.normalize_time(value)
55
+ return nil if value.nil?
56
+ value.respond_to?(:iso8601) ? value.iso8601 : value.to_s
57
+ end
58
+
59
+ def self.from_h(hash)
60
+ new(
61
+ id: hash[:id] || hash["id"],
62
+ user_id: hash[:user_id] || hash["user_id"],
63
+ steps: hash[:steps] || hash["steps"] || [],
64
+ summary: hash[:summary] || hash["summary"],
65
+ outcome: hash[:outcome] || hash["outcome"],
66
+ importance: hash[:importance] || hash["importance"] || 0.5,
67
+ provenance: hash[:provenance] || hash["provenance"],
68
+ created_at: parse_created_at(hash[:created_at] || hash["created_at"])
69
+ )
70
+ end
71
+
72
+ def self.parse_created_at(value)
73
+ return value if value.nil? || value.is_a?(Time)
74
+ Time.parse(value.to_s)
75
+ rescue ArgumentError
76
+ nil
77
+ end
78
+
79
+ def to_h
80
+ {
81
+ id: id,
82
+ user_id: user_id,
83
+ steps: steps,
84
+ summary: summary,
85
+ outcome: outcome,
86
+ importance: importance,
87
+ provenance: provenance,
88
+ created_at: created_at.respond_to?(:iso8601) ? created_at.iso8601(6) : created_at
89
+ }
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "episode"
4
+ require_relative "storage"
5
+
6
+ module Llmemory
7
+ module LongTerm
8
+ module Episodic
9
+ # Episodic long-term memory: records agent trajectories and retrieves them
10
+ # by recency, importance and relevance. Designed to coexist with semantic
11
+ # memory (file/graph), not replace it, and to feed reflection (P2), which
12
+ # distills episodes into semantic knowledge.
13
+ #
14
+ # Deliberately LLM-free: recording and retrieval are deterministic. Higher
15
+ # order summarization belongs to reflection.
16
+ class Memory
17
+ attr_reader :user_id, :storage
18
+
19
+ def initialize(user_id:, storage: nil)
20
+ @user_id = user_id
21
+ @storage = storage || Storages.build
22
+ end
23
+
24
+ # Records a trajectory. `steps` is an array of hashes with any of
25
+ # :observation, :action, :result, :timestamp. Returns the episode id.
26
+ def record_episode(steps:, summary: nil, outcome: nil, importance: 0.5)
27
+ episode = Episode.new(
28
+ id: nil,
29
+ user_id: @user_id,
30
+ steps: steps,
31
+ summary: summary || derive_summary(steps),
32
+ outcome: outcome,
33
+ importance: importance
34
+ )
35
+ provenance = Llmemory::Provenance.from_text_fingerprint(
36
+ episode.searchable_text, method: "episode_recording", confidence: episode.importance
37
+ )
38
+ record = episode.to_h.merge(provenance: provenance)
39
+ @storage.save_episode(@user_id, record)
40
+ end
41
+
42
+ def recent_episodes(limit: 10)
43
+ @storage.list_episodes(@user_id, limit: limit).map { |e| Episode.from_h(e) }
44
+ end
45
+
46
+ def episodes(limit: nil)
47
+ @storage.list_episodes(@user_id, limit: limit).map { |e| Episode.from_h(e) }
48
+ end
49
+
50
+ def find_episode(id)
51
+ raw = @storage.get_episode(@user_id, id)
52
+ raw && Episode.from_h(raw)
53
+ end
54
+
55
+ def count
56
+ @storage.count_episodes(@user_id)
57
+ end
58
+
59
+ # Retrieval Engine integration. Returns candidates shaped like the other
60
+ # long-term memories so the Engine can rank episodes by relevance,
61
+ # recency (temporal decay) and importance (P3), with provenance (P10).
62
+ def search_candidates(query, user_id: nil, top_k: 20)
63
+ uid = user_id || @user_id
64
+ return [] unless uid == @user_id
65
+
66
+ @storage.search_episodes(uid, query).first(top_k).map do |e|
67
+ episode = Episode.from_h(e)
68
+ {
69
+ text: episode.summary.to_s.empty? ? episode.searchable_text : episode.summary,
70
+ timestamp: episode.created_at,
71
+ score: 1.0,
72
+ importance: episode.importance,
73
+ evergreen: false,
74
+ provenance: e[:provenance] || e["provenance"]
75
+ }
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ # Cheap, deterministic summary when the caller does not provide one.
82
+ # LLM-based summarization is reflection's job (P2).
83
+ def derive_summary(steps)
84
+ normalized = Episode.normalize_steps(steps)
85
+ return nil if normalized.empty?
86
+ actions = normalized.filter_map { |s| s[:action] }.reject { |a| a.to_s.strip.empty? }
87
+ return nil if actions.empty?
88
+ "Episode with #{normalized.size} step(s): #{actions.join(' -> ')}"
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "storages/base"
4
+ require_relative "storages/memory_storage"
5
+ require_relative "storages/file_storage"
6
+
7
+ module Llmemory
8
+ module LongTerm
9
+ module Episodic
10
+ # Backward compatibility: Storage points to the in-memory backend.
11
+ Storage = Storages::MemoryStorage
12
+
13
+ module Storages
14
+ def self.build(store: nil, base_path: nil)
15
+ case (store || Llmemory.configuration.long_term_store).to_s.to_sym
16
+ when :memory
17
+ MemoryStorage.new
18
+ when :file
19
+ FileStorage.new(base_path: base_path || Llmemory.configuration.long_term_storage_path)
20
+ when :postgres, :database, :active_record, :activerecord
21
+ raise NotImplementedError,
22
+ "Episodic SQL/ActiveRecord storage is not implemented yet; use :memory or :file " \
23
+ "(or pass an explicit storage instance)."
24
+ else
25
+ MemoryStorage.new
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module LongTerm
5
+ module Episodic
6
+ module Storages
7
+ # Storage contract for episodic memory. Implementations persist Episode
8
+ # hashes and expose recency-ordered listing plus keyword search so the
9
+ # retrieval Engine can rank episodes alongside other memory types.
10
+ class Base
11
+ def save_episode(user_id, episode)
12
+ raise NotImplementedError, "#{self.class}#save_episode must be implemented"
13
+ end
14
+
15
+ def get_episode(user_id, id)
16
+ raise NotImplementedError, "#{self.class}#get_episode must be implemented"
17
+ end
18
+
19
+ # Newest first. Optionally capped by limit.
20
+ def list_episodes(user_id, limit: nil)
21
+ raise NotImplementedError, "#{self.class}#list_episodes must be implemented"
22
+ end
23
+
24
+ def search_episodes(user_id, query)
25
+ raise NotImplementedError, "#{self.class}#search_episodes must be implemented"
26
+ end
27
+
28
+ def count_episodes(user_id)
29
+ raise NotImplementedError, "#{self.class}#count_episodes must be implemented"
30
+ end
31
+
32
+ def list_users
33
+ raise NotImplementedError, "#{self.class}#list_users must be implemented"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "time"
6
+ require_relative "base"
7
+
8
+ module Llmemory
9
+ module LongTerm
10
+ module Episodic
11
+ module Storages
12
+ class FileStorage < Base
13
+ def initialize(base_path: nil)
14
+ @base_path = base_path || Llmemory.configuration.long_term_storage_path || "./llmemory_data"
15
+ @base_path = File.expand_path(@base_path)
16
+ end
17
+
18
+ def save_episode(user_id, episode)
19
+ id = episode[:id] || episode["id"] || "ep_#{next_seq(user_id)}"
20
+ data = stringify_for_json(episode).merge("id" => id, "user_id" => user_id)
21
+ data["created_at"] ||= Time.now.iso8601
22
+ File.write(episode_path(user_id, id), JSON.generate(data))
23
+ id
24
+ end
25
+
26
+ def get_episode(user_id, id)
27
+ path = episode_path(user_id, id)
28
+ return nil unless File.file?(path)
29
+ load_episode(path)
30
+ end
31
+
32
+ def list_episodes(user_id, limit: nil)
33
+ sorted = all_episodes(user_id).sort_by { |e| e[:created_at] }.reverse
34
+ limit && limit.to_i.positive? ? sorted.first(limit.to_i) : sorted
35
+ end
36
+
37
+ def search_episodes(user_id, query)
38
+ q = query.to_s.downcase
39
+ return list_episodes(user_id) if q.strip.empty?
40
+ all_episodes(user_id).select { |e| episode_text(e).downcase.include?(q) }
41
+ end
42
+
43
+ def count_episodes(user_id)
44
+ dir = user_path(user_id, "episodes")
45
+ return 0 unless Dir.exist?(dir)
46
+ Dir.children(dir).count { |f| f.end_with?(".json") }
47
+ end
48
+
49
+ def list_users
50
+ return [] unless Dir.exist?(@base_path)
51
+ Dir.children(@base_path).select { |d| Dir.exist?(File.join(@base_path, d, "episodes")) }
52
+ end
53
+
54
+ private
55
+
56
+ def all_episodes(user_id)
57
+ dir = user_path(user_id, "episodes")
58
+ return [] unless Dir.exist?(dir)
59
+ Dir.children(dir).select { |f| f.end_with?(".json") }.map { |f| load_episode(File.join(dir, f)) }.compact
60
+ end
61
+
62
+ def load_episode(path)
63
+ data = JSON.parse(File.read(path), symbolize_names: true)
64
+ data[:created_at] = parse_time(data[:created_at])
65
+ data
66
+ rescue JSON::ParserError
67
+ nil
68
+ end
69
+
70
+ def episode_text(episode)
71
+ parts = [episode[:summary], episode[:outcome]]
72
+ Array(episode[:steps]).each do |s|
73
+ next unless s.is_a?(Hash)
74
+ parts << s[:observation] << s[:action] << s[:result]
75
+ end
76
+ parts.compact.join("\n")
77
+ end
78
+
79
+ def stringify_for_json(episode)
80
+ JSON.parse(JSON.generate(episode))
81
+ end
82
+
83
+ def user_path(user_id, *parts)
84
+ safe = user_id.to_s.gsub(%r{[^\w\-.]}, "_")
85
+ File.join(@base_path, safe, *parts)
86
+ end
87
+
88
+ def episode_path(user_id, id)
89
+ dir = user_path(user_id, "episodes")
90
+ FileUtils.mkdir_p(dir)
91
+ File.join(dir, "#{id}.json")
92
+ end
93
+
94
+ def meta_path(user_id)
95
+ FileUtils.mkdir_p(user_path(user_id))
96
+ File.join(user_path(user_id), "meta.json")
97
+ end
98
+
99
+ def next_seq(user_id)
100
+ path = meta_path(user_id)
101
+ meta = File.file?(path) ? JSON.parse(File.read(path)) : {}
102
+ meta["episode_id_seq"] = (meta["episode_id_seq"] || 0) + 1
103
+ File.write(path, JSON.generate(meta))
104
+ meta["episode_id_seq"]
105
+ end
106
+
107
+ def parse_time(val)
108
+ return val if val.is_a?(Time)
109
+ Time.parse(val.to_s)
110
+ rescue ArgumentError
111
+ Time.now
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Llmemory
6
+ module LongTerm
7
+ module Episodic
8
+ module Storages
9
+ class MemoryStorage < Base
10
+ def initialize
11
+ @episodes = Hash.new { |h, k| h[k] = [] }
12
+ @seq = 0
13
+ end
14
+
15
+ def save_episode(user_id, episode)
16
+ @seq += 1
17
+ id = episode[:id] || episode["id"] || "ep_#{@seq}"
18
+ record = symbolize(episode).merge(id: id, user_id: user_id)
19
+ record[:created_at] ||= Time.now
20
+ @episodes[user_id] << record
21
+ id
22
+ end
23
+
24
+ def get_episode(user_id, id)
25
+ @episodes[user_id].find { |e| e[:id] == id }
26
+ end
27
+
28
+ def list_episodes(user_id, limit: nil)
29
+ sorted = @episodes[user_id].sort_by { |e| e[:created_at] }.reverse
30
+ limit && limit.to_i.positive? ? sorted.first(limit.to_i) : sorted
31
+ end
32
+
33
+ def search_episodes(user_id, query)
34
+ q = query.to_s.downcase
35
+ return list_episodes(user_id) if q.strip.empty?
36
+ @episodes[user_id].select { |e| episode_text(e).downcase.include?(q) }
37
+ end
38
+
39
+ def count_episodes(user_id)
40
+ @episodes[user_id].size
41
+ end
42
+
43
+ def list_users
44
+ @episodes.keys
45
+ end
46
+
47
+ private
48
+
49
+ def symbolize(hash)
50
+ hash.each_with_object({}) { |(k, v), acc| acc[k.to_sym] = v }
51
+ end
52
+
53
+ def episode_text(episode)
54
+ parts = [episode[:summary], episode[:outcome]]
55
+ Array(episode[:steps]).each do |s|
56
+ next unless s.is_a?(Hash)
57
+ parts << (s[:observation] || s["observation"])
58
+ parts << (s[:action] || s["action"])
59
+ parts << (s[:result] || s["result"])
60
+ end
61
+ parts.compact.join("\n")
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "episodic/episode"
4
+ require_relative "episodic/storage"
5
+ require_relative "episodic/memory"
6
+
7
+ module Llmemory
8
+ module LongTerm
9
+ module Episodic
10
+ end
11
+ end
12
+ end
@@ -85,6 +85,21 @@ module Llmemory
85
85
  out
86
86
  end
87
87
 
88
+ # Stores a single fact produced outside the extraction flow (e.g. by
89
+ # reflection over episodes), preserving caller-supplied provenance so the
90
+ # insight remains traceable to its source. Returns the item id.
91
+ def remember_fact(content:, category: "general", importance: 0.6, provenance: nil)
92
+ return nil if content.to_s.strip.empty?
93
+ @storage.save_item(
94
+ @user_id,
95
+ category: category.to_s,
96
+ content: content.to_s,
97
+ source_resource_id: nil,
98
+ importance: importance,
99
+ provenance: provenance
100
+ )
101
+ end
102
+
88
103
  attr_reader :storage, :user_id
89
104
 
90
105
  private
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "long_term/file_based"
4
4
  require_relative "long_term/graph_based"
5
+ require_relative "long_term/episodic"
5
6
 
6
7
  module Llmemory
7
8
  module LongTerm
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Llmemory
6
+ module Reflection
7
+ # Reflection distills an agent's recent episodes (episodic memory) into
8
+ # durable, higher-order insights and writes them to semantic memory. This is
9
+ # CoALA's "updating semantic memory with knowledge" (Reflexion / Generative
10
+ # Agents): unlike one-shot extraction from raw text, it reasons over lived
11
+ # experience to generalize lessons and patterns.
12
+ #
13
+ # Each insight is stored with provenance { method: "reflection",
14
+ # sources: [{ type: "episode", id: ... }] } so it stays traceable to the
15
+ # experiences that produced it.
16
+ #
17
+ # `semantic` must respond to:
18
+ # remember_fact(content:, category:, importance:, provenance:)
19
+ # (FileBased::Memory implements this; graph-based is a future target.)
20
+ class Reflector
21
+ DEFAULT_CATEGORY = "insights"
22
+ DEFAULT_IMPORTANCE = 0.6
23
+
24
+ def initialize(episodic:, semantic:, llm: nil)
25
+ @episodic = episodic
26
+ @semantic = semantic
27
+ @llm = llm || Llmemory::LLM.client
28
+ end
29
+
30
+ # Reflects over the most recent `window` episodes and writes the resulting
31
+ # insights to semantic memory. Returns the ids of the stored insights.
32
+ def reflect(window: 10, category: DEFAULT_CATEGORY)
33
+ episodes = @episodic.recent_episodes(limit: window)
34
+ return [] if episodes.empty?
35
+
36
+ insights = distill(episodes)
37
+ return [] if insights.empty?
38
+
39
+ sources = episodes.map(&:id).compact.map { |id| { type: "episode", id: id } }
40
+
41
+ insights.filter_map do |insight|
42
+ provenance = Llmemory::Provenance.build(
43
+ method: "reflection",
44
+ sources: sources,
45
+ confidence: insight[:confidence]
46
+ )
47
+ @semantic.remember_fact(
48
+ content: insight[:content],
49
+ category: category,
50
+ importance: insight[:confidence] || DEFAULT_IMPORTANCE,
51
+ provenance: provenance
52
+ )
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def distill(episodes)
59
+ response = @llm.invoke(build_prompt(episodes))
60
+ parse_insights(response)
61
+ rescue Llmemory::LLMError
62
+ []
63
+ end
64
+
65
+ def build_prompt(episodes)
66
+ episodes_text = episodes.each_with_index.map do |ep, i|
67
+ "Episode #{i + 1} (outcome: #{ep.outcome || 'n/a'}):\n#{ep.searchable_text}"
68
+ end.join("\n\n")
69
+
70
+ <<~PROMPT
71
+ You are reflecting on an agent's recent experiences to distill durable,
72
+ higher-order insights: lessons learned, recurring patterns, and stable
73
+ preferences that will help in future situations. Generalize; do not
74
+ restate raw events.
75
+
76
+ Recent episodes:
77
+ #{episodes_text}
78
+
79
+ Return a JSON array of objects with "content" (the insight) and
80
+ "confidence" (0-1) keys. Return an empty array if nothing durable can
81
+ be concluded.
82
+ Example: [{"content": "Rolling back on deploy failure reliably restores service", "confidence": 0.8}]
83
+ PROMPT
84
+ end
85
+
86
+ def parse_insights(response)
87
+ json = extract_json_array(response)
88
+ return [] unless json
89
+
90
+ json.filter_map do |item|
91
+ next nil unless item.is_a?(Hash)
92
+ content = item["content"] || item[:content]
93
+ next nil if content.to_s.strip.empty?
94
+ { content: content.to_s, confidence: normalize_confidence(item["confidence"] || item[:confidence]) }
95
+ end
96
+ end
97
+
98
+ def normalize_confidence(value)
99
+ return nil if value.nil?
100
+ v = value.to_f
101
+ v.between?(0, 1) ? v : nil
102
+ end
103
+
104
+ def extract_json_array(response)
105
+ response = response.to_s.strip
106
+ start_idx = response.index("[")
107
+ end_idx = response.rindex("]")
108
+ return nil unless start_idx && end_idx
109
+
110
+ JSON.parse(response[start_idx..end_idx])
111
+ rescue JSON::ParserError
112
+ nil
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "reflection/reflector"
4
+
5
+ module Llmemory
6
+ module Reflection
7
+ end
8
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Llmemory
4
- VERSION = "0.1.16"
4
+ VERSION = "0.1.17"
5
5
  end
data/lib/llmemory.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "llmemory/retrieval"
10
10
  require_relative "llmemory/vector_store"
11
11
  require_relative "llmemory/maintenance"
12
12
  require_relative "llmemory/extractors"
13
+ require_relative "llmemory/reflection"
13
14
  require_relative "llmemory/memory"
14
15
 
15
16
  module Llmemory
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llmemory
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.16
4
+ version: 0.1.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - llmemory
@@ -152,6 +152,13 @@ files:
152
152
  - lib/llmemory/llm/base.rb
153
153
  - lib/llmemory/llm/openai.rb
154
154
  - lib/llmemory/long_term.rb
155
+ - lib/llmemory/long_term/episodic.rb
156
+ - lib/llmemory/long_term/episodic/episode.rb
157
+ - lib/llmemory/long_term/episodic/memory.rb
158
+ - lib/llmemory/long_term/episodic/storage.rb
159
+ - lib/llmemory/long_term/episodic/storages/base.rb
160
+ - lib/llmemory/long_term/episodic/storages/file_storage.rb
161
+ - lib/llmemory/long_term/episodic/storages/memory_storage.rb
155
162
  - lib/llmemory/long_term/file_based.rb
156
163
  - lib/llmemory/long_term/file_based/category.rb
157
164
  - lib/llmemory/long_term/file_based/item.rb
@@ -196,6 +203,8 @@ files:
196
203
  - lib/llmemory/memory.rb
197
204
  - lib/llmemory/noise_filter.rb
198
205
  - lib/llmemory/provenance.rb
206
+ - lib/llmemory/reflection.rb
207
+ - lib/llmemory/reflection/reflector.rb
199
208
  - lib/llmemory/retrieval.rb
200
209
  - lib/llmemory/retrieval/bm25_scorer.rb
201
210
  - lib/llmemory/retrieval/context_assembler.rb