llmemory 0.2.0 → 0.2.2

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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +54 -3
  3. data/app/controllers/llmemory/dashboard/application_controller.rb +15 -1
  4. data/app/controllers/llmemory/dashboard/episodic_controller.rb +22 -0
  5. data/app/controllers/llmemory/dashboard/forget_log_controller.rb +12 -0
  6. data/app/controllers/llmemory/dashboard/maintenance_controller.rb +92 -0
  7. data/app/controllers/llmemory/dashboard/procedural_controller.rb +22 -0
  8. data/app/controllers/llmemory/dashboard/reflection_controller.rb +37 -0
  9. data/app/controllers/llmemory/dashboard/working_controller.rb +14 -0
  10. data/app/views/llmemory/dashboard/episodic/index.html.erb +37 -0
  11. data/app/views/llmemory/dashboard/forget_log/show.html.erb +23 -0
  12. data/app/views/llmemory/dashboard/maintenance/show.html.erb +65 -0
  13. data/app/views/llmemory/dashboard/procedural/index.html.erb +38 -0
  14. data/app/views/llmemory/dashboard/reflection/show.html.erb +29 -0
  15. data/app/views/llmemory/dashboard/users/show.html.erb +16 -0
  16. data/app/views/llmemory/dashboard/working/show.html.erb +20 -0
  17. data/config/routes.rb +14 -0
  18. data/lib/generators/llmemory/install/templates/create_llmemory_tables.rb +22 -0
  19. data/lib/llmemory/cli/commands/base.rb +8 -0
  20. data/lib/llmemory/cli/commands/episodic.rb +42 -0
  21. data/lib/llmemory/cli/commands/forget_log.rb +36 -0
  22. data/lib/llmemory/cli/commands/maintain.rb +62 -0
  23. data/lib/llmemory/cli/commands/mine_skills.rb +50 -0
  24. data/lib/llmemory/cli/commands/procedural.rb +44 -0
  25. data/lib/llmemory/cli/commands/working.rb +31 -0
  26. data/lib/llmemory/cli.rb +18 -0
  27. data/lib/llmemory/configuration.rb +11 -1
  28. data/lib/llmemory/instrumentation.rb +33 -0
  29. data/lib/llmemory/llm/anthropic.rb +19 -15
  30. data/lib/llmemory/llm/openai.rb +16 -12
  31. data/lib/llmemory/long_term/episodic/memory.rb +94 -26
  32. data/lib/llmemory/long_term/episodic/storage.rb +7 -5
  33. data/lib/llmemory/long_term/episodic/storages/active_record_models.rb +17 -0
  34. data/lib/llmemory/long_term/episodic/storages/active_record_storage.rb +103 -0
  35. data/lib/llmemory/long_term/episodic/storages/base.rb +15 -2
  36. data/lib/llmemory/long_term/episodic/storages/database_storage.rb +156 -0
  37. data/lib/llmemory/long_term/episodic/storages/file_storage.rb +28 -8
  38. data/lib/llmemory/long_term/episodic/storages/memory_storage.rb +36 -6
  39. data/lib/llmemory/long_term/file_based/memory.rb +12 -4
  40. data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +15 -6
  41. data/lib/llmemory/long_term/file_based/storages/base.rb +2 -2
  42. data/lib/llmemory/long_term/file_based/storages/database_storage.rb +20 -8
  43. data/lib/llmemory/long_term/file_based/storages/file_storage.rb +6 -6
  44. data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +6 -6
  45. data/lib/llmemory/long_term/graph_based/memory.rb +89 -64
  46. data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +4 -2
  47. data/lib/llmemory/long_term/graph_based/storages/base.rb +2 -2
  48. data/lib/llmemory/long_term/graph_based/storages/memory_storage.rb +4 -2
  49. data/lib/llmemory/long_term/procedural/memory.rb +97 -30
  50. data/lib/llmemory/long_term/procedural/skill.rb +6 -2
  51. data/lib/llmemory/long_term/procedural/storage.rb +7 -5
  52. data/lib/llmemory/long_term/procedural/storages/active_record_models.rb +17 -0
  53. data/lib/llmemory/long_term/procedural/storages/active_record_storage.rb +114 -0
  54. data/lib/llmemory/long_term/procedural/storages/base.rb +14 -1
  55. data/lib/llmemory/long_term/procedural/storages/database_storage.rb +169 -0
  56. data/lib/llmemory/long_term/procedural/storages/file_storage.rb +29 -9
  57. data/lib/llmemory/long_term/procedural/storages/memory_storage.rb +37 -7
  58. data/lib/llmemory/maintenance/cognitive_pass.rb +109 -0
  59. data/lib/llmemory/maintenance/ttl_expiry.rb +50 -0
  60. data/lib/llmemory/maintenance.rb +2 -0
  61. data/lib/llmemory/mcp/server.rb +17 -1
  62. data/lib/llmemory/mcp/tools/memory_episode_record.rb +48 -0
  63. data/lib/llmemory/mcp/tools/memory_episodes.rb +43 -0
  64. data/lib/llmemory/mcp/tools/memory_forget.rb +53 -0
  65. data/lib/llmemory/mcp/tools/memory_maintain.rb +53 -0
  66. data/lib/llmemory/mcp/tools/memory_mine_skills.rb +53 -0
  67. data/lib/llmemory/mcp/tools/memory_retrieve.rb +10 -2
  68. data/lib/llmemory/mcp/tools/memory_skill_register.rb +35 -0
  69. data/lib/llmemory/mcp/tools/memory_skill_report.rb +35 -0
  70. data/lib/llmemory/mcp/tools/memory_skills.rb +43 -0
  71. data/lib/llmemory/memory.rb +48 -3
  72. data/lib/llmemory/memory_module.rb +13 -6
  73. data/lib/llmemory/reflection/reflector.rb +24 -20
  74. data/lib/llmemory/retrieval/bm25_scorer.rb +1 -1
  75. data/lib/llmemory/retrieval/engine.rb +25 -16
  76. data/lib/llmemory/retrieval/mmr_reranker.rb +1 -1
  77. data/lib/llmemory/short_term/session_lifecycle.rb +19 -3
  78. data/lib/llmemory/skill_mining/miner.rb +163 -0
  79. data/lib/llmemory/skill_mining.rb +8 -0
  80. data/lib/llmemory/tokenizer.rb +27 -0
  81. data/lib/llmemory/vector_store/active_record_store.rb +4 -3
  82. data/lib/llmemory/vector_store/openai_embeddings.rb +11 -7
  83. data/lib/llmemory/vector_store.rb +14 -0
  84. data/lib/llmemory/version.rb +1 -1
  85. data/lib/llmemory.rb +3 -0
  86. metadata +39 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c302656888e6373faedb5732525f76d118fd98fb67ece22278d886faec5ba3cd
4
- data.tar.gz: ec0897226ec378e51e86e01cb66e938fbeea3db7e4c49911d7e3d08a08959d07
3
+ metadata.gz: fdcf202249038554cae18d79da76c261a9c7a80687081126ce985562ef8607ae
4
+ data.tar.gz: 9b28b0ba29d4444712c2592a808f6b08bbf38f804c031e6b8b246914f7b86699
5
5
  SHA512:
6
- metadata.gz: 6cee9a244e42f198b9fd491d164d85d2524a30d6cf9ca0b4a4da3f1a012f7e2b4643ba1e4dd6838ba7ce32517d5425b77f62df1cb1334ac34d162a273cd6400f
7
- data.tar.gz: 6d46031eda16a52b7c8b42b364dc5351a0eb18f0eeaa135271a3976a0ebba39db439e74fafa981b9cea062d70e2bd006b4576ccaccfff29bfd6ec6fe8011a411
6
+ metadata.gz: 4bddb0f7e9a4bfe6cfd488a341efce98ab4194c36fd6ebcb22b024db338224d0191e3bfb5bbc9638ad43092cac66bb29d159e87d81b97f6d17c8ee82b400c716
7
+ data.tar.gz: 8e5fb5edddabce0b1b57903282bb868a076d1595c4d187c973ccb7a7fa61f11847c9889e185459438423553aee0bf52f216e3bbdf0d9ac870f9f8196c230c1f1
data/README.md CHANGED
@@ -74,6 +74,11 @@ Llmemory.configure do |config|
74
74
  config.importance_weight = 1.0 # how strongly importance multiplies the score (0 = ignore)
75
75
  config.retrieval_feedback_weight = 0.5 # how strongly useful/harmful feedback shifts ranking (0 = ignore)
76
76
 
77
+ # Semantic (embedding) retrieval for episodic/procedural memory (opt-in;
78
+ # default off keeps them deterministic and network-free)
79
+ config.episodic_vector_enabled = false
80
+ config.procedural_vector_enabled = false
81
+
77
82
  # Pre-compaction memory flush (prevents knowledge loss when compacting)
78
83
  config.memory_flush_enabled = true
79
84
  config.memory_flush_threshold_tokens = 4000
@@ -129,7 +134,7 @@ rails g llmemory:install
129
134
  rails db:migrate
130
135
  ```
131
136
 
132
- La migración crea las tablas de long-term file-based (resources, items, categories), short-term (checkpoints) y, para graph-based, nodos, aristas y embeddings (`llmemory_nodes`, `llmemory_edges`, `llmemory_embeddings`). Para embeddings se usa pgvector; asegúrate de tener la extensión `vector` en PostgreSQL. Para usar ambas con ActiveRecord:
137
+ La migración crea las tablas de long-term file-based (resources, items, categories), short-term (checkpoints), episódica y procedural (`llmemory_episodes`, `llmemory_skills`; columna `data` JSONB) y, para graph-based, nodos, aristas y embeddings (`llmemory_nodes`, `llmemory_edges`, `llmemory_embeddings`). Para embeddings se usa pgvector; asegúrate de tener la extensión `vector` en PostgreSQL. Para usar ambas con ActiveRecord:
133
138
 
134
139
  ```ruby
135
140
  # config/application.rb o config/initializers/llmemory.rb
@@ -211,10 +216,10 @@ llmemory implements the memory and internal-action concepts from [CoALA — Cogn
211
216
  | Procedural memory | `Llmemory::LongTerm::Procedural::Memory` |
212
217
  | Reasoning action | `Llmemory::Actions::Reason` |
213
218
  | Retrieval action | `Retrieval::Engine` (+ feedback, iterative) |
214
- | Learning action | `memorize` / `record_episode` / `register_skill` / reflection |
219
+ | Learning action | `memorize` / `record_episode` / `register_skill` / reflection / skill mining |
215
220
  | Uniform interface | `Llmemory::MemoryModule` (`read`/`write`/`list`/`stats`/`forget`) |
216
221
 
217
- All three long-term memories below are **additive** — episodic and procedural coexist with semantic memory rather than replacing it. Episodic/procedural ship with `:memory` and `:file` backends (SQL/ActiveRecord and vector search are roadmap items); retrieval there is keyword-based.
222
+ All three long-term memories below are **additive** — episodic and procedural coexist with semantic memory rather than replacing it. They support `:memory`, `:file`, `:postgres` and `:active_record` backends. Retrieval is keyword-based by default (tokenized, so multi-word queries work); semantic (embedding) retrieval is **opt-in** via `config.episodic_vector_enabled` / `config.procedural_vector_enabled` (or by injecting a `vector_store:`), which makes `search_candidates` hybrid (vector + keyword).
218
223
 
219
224
  ### Working memory (structured, persists across LLM calls)
220
225
 
@@ -305,6 +310,39 @@ skills.find_skill("revert deploy") # best match (a Skill)
305
310
  skills.report_outcome(id, success: true) # feeds ranking + adaptive retrieval
306
311
  ```
307
312
 
313
+ ### Skill mining (episodic → procedural)
314
+
315
+ Rather than writing every skill by hand, mine them from successful episode trajectories (Voyager's contribution: procedural memory grows from lived experience). Mining is **human-in-the-loop by default** — it returns proposals and writes nothing until you opt in.
316
+
317
+ ```ruby
318
+ miner = Llmemory::SkillMining::Miner.new(episodic: episodic, procedural: skills)
319
+
320
+ proposals = miner.mine(window: 20) # => [{ name:, kind:, body:, description:, confidence: }, ...]
321
+ miner.mine(window: 20, outcomes: ["success"]) # deterministic pre-filter by outcome label
322
+ ids = miner.mine(window: 20, auto_register: true) # register proposals straight away
323
+
324
+ # Registered skills carry provenance { method: "skill_mining", sources: [{ type: "episode", id: ... }] }.
325
+ # From the unified API: memory.mine_skills!(auto_register: true)
326
+ ```
327
+
328
+ ### Cognitive maintenance pass (closing the loop)
329
+
330
+ One scheduled step that runs the whole learning loop — consolidate → reflect → mine skills → TTL expiry — for a user. Each step is isolated: a failure is captured in the report and never aborts the others.
331
+
332
+ ```ruby
333
+ report = Llmemory::Maintenance::CognitivePass.run!(
334
+ "u1",
335
+ reflect: true, mine_skills: true, expire: true, # toggle steps
336
+ reflection_window: 10, mining_window: 20
337
+ )
338
+ # => { consolidated:, insights: [...], mined: [...], expired: { episodic:, procedural: }, errors: {} }
339
+
340
+ # From the unified API (wires in the live session, so consolidate! runs too):
341
+ memory.maintain!(mine_skills: true)
342
+ ```
343
+
344
+ `mine_skills` defaults to `config.skill_mining_enabled` (default `false`). Run it from cron or a Rails job. Components (`episodic:`, `procedural:`, `semantic:`, `llm:`) are injectable; built per-config when omitted.
345
+
308
346
  ### Uniform interface (MemoryModule)
309
347
 
310
348
  The queryable long-term memories (file, graph, episodic, procedural) share one agent-facing contract, so a framework can treat them polymorphically:
@@ -484,6 +522,14 @@ llmemory edges USER_ID [--subject NODE_ID] [--limit N]
484
522
  llmemory graph USER_ID [--format dot|json]
485
523
  llmemory search USER_ID "query" [--type short|long|all]
486
524
  llmemory stats [USER_ID]
525
+
526
+ # Cognitive memory (CoALA)
527
+ llmemory episodes USER_ID [--limit N]
528
+ llmemory skills USER_ID [--limit N]
529
+ llmemory working USER_ID SESSION_ID
530
+ llmemory forget-log USER_ID
531
+ llmemory mine-skills USER_ID [--window N] [--outcomes success,recovered] [--register]
532
+ llmemory maintain USER_ID [--[no-]reflect] [--mine-skills] [--[no-]expire] [--window N]
487
533
  ```
488
534
 
489
535
  Use `--store TYPE` where applicable to override the configured store (e.g. `memory`, `redis`, `postgres`, `active_record` for short-term; same or `file` for long-term file-based).
@@ -610,6 +656,11 @@ MCP_TOKEN=your-secret-token llmemory mcp serve --http --port 443 \
610
656
  | `memory_consolidate` | Extract facts from conversation to long-term |
611
657
  | `memory_stats` | Get memory statistics for a user |
612
658
  | `memory_info` | Documentation on how to use the tools |
659
+ | `memory_episode_record` / `memory_episodes` | Record / list episodic trajectories |
660
+ | `memory_skill_register` / `memory_skill_report` / `memory_skills` | Register / outcome-track / list procedural skills |
661
+ | `memory_forget` | Forget entries by id (audited) across any memory type |
662
+ | `memory_mine_skills` | Mine reusable skills from episodes (proposals by default; `auto_register` to save) |
663
+ | `memory_maintain` | Run the cognitive maintenance pass (reflect → mine → expire) and return a report |
613
664
 
614
665
  ### Configuration for Claude Code
615
666
 
@@ -3,7 +3,9 @@
3
3
  module Llmemory
4
4
  module Dashboard
5
5
  class ApplicationController < ActionController::Base
6
- helper_method :short_term_store, :file_based_storage, :graph_based_storage, :file_based?, :graph_based?
6
+ helper_method :short_term_store, :file_based_storage, :graph_based_storage,
7
+ :episodic_storage, :procedural_storage, :forget_log,
8
+ :file_based?, :graph_based?
7
9
 
8
10
  protected
9
11
 
@@ -19,6 +21,18 @@ module Llmemory
19
21
  @graph_based_storage ||= build_graph_based_storage
20
22
  end
21
23
 
24
+ def episodic_storage
25
+ @episodic_storage ||= Llmemory::LongTerm::Episodic::Storages.build
26
+ end
27
+
28
+ def procedural_storage
29
+ @procedural_storage ||= Llmemory::LongTerm::Procedural::Storages.build
30
+ end
31
+
32
+ def forget_log
33
+ @forget_log ||= Llmemory::ForgetLog.new
34
+ end
35
+
22
36
  def long_term_type
23
37
  Llmemory.configuration.long_term_type.to_s
24
38
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Dashboard
5
+ class EpisodicController < ApplicationController
6
+ def index
7
+ @user_id = params[:user_id]
8
+ @limit = (params[:limit].presence || 50).to_i
9
+ @offset = (params[:offset].presence || 0).to_i
10
+ @episodes = episodic_storage.list_episodes(@user_id, limit: @limit, offset: @offset)
11
+ @total = episodic_storage.count_episodes(@user_id)
12
+ end
13
+
14
+ def forget
15
+ memory = Llmemory::LongTerm::Episodic::Memory.new(user_id: params[:user_id], storage: episodic_storage)
16
+ mode = params[:mode].to_s == "hard" ? :hard : :soft
17
+ memory.forget(ids: [params[:id]], reason: params[:reason], mode: mode)
18
+ redirect_to user_episodic_path(params[:user_id]), notice: "Forgot episode #{params[:id]} (#{mode})."
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Dashboard
5
+ class ForgetLogController < ApplicationController
6
+ def show
7
+ @user_id = params[:user_id]
8
+ @entries = forget_log.entries(@user_id)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,92 @@
1
+ module Llmemory
2
+ module Dashboard
3
+ # Cognitive maintenance surface (SF20): trigger a maintenance pass and review
4
+ # mined-skill proposals before registering them (human-in-the-loop).
5
+ class MaintenanceController < ApplicationController
6
+ def show
7
+ @user_id = params[:user_id]
8
+ @window = (params[:window].presence || 10).to_i
9
+ @recent_episodes = episodic_storage.list_episodes(@user_id, limit: @window)
10
+ @proposals = session_proposals
11
+ end
12
+
13
+ # Runs the full cognitive pass (reflect -> mine -> expire) and reports.
14
+ def run
15
+ user_id = params[:user_id]
16
+ window = (params[:window].presence || 10).to_i
17
+ report = Llmemory::Maintenance::CognitivePass.run!(
18
+ user_id,
19
+ episodic: episodic_memory(user_id),
20
+ procedural: procedural_memory(user_id),
21
+ semantic: build_semantic_memory(user_id),
22
+ mine_skills: params[:mine_skills].present?,
23
+ reflection_window: window
24
+ )
25
+ redirect_to user_maintenance_path(user_id), notice: pass_notice(report)
26
+ rescue Llmemory::LLMError => e
27
+ redirect_to user_maintenance_path(user_id), alert: "Maintenance pass failed: #{e.message}"
28
+ end
29
+
30
+ # Mines skills WITHOUT registering — returns proposals for review.
31
+ def mine
32
+ user_id = params[:user_id]
33
+ window = (params[:window].presence || 20).to_i
34
+ proposals = Llmemory::SkillMining::Miner.new(
35
+ episodic: episodic_memory(user_id), procedural: procedural_memory(user_id)
36
+ ).mine(window: window, auto_register: false)
37
+ store_proposals(proposals)
38
+ redirect_to user_maintenance_path(user_id), notice: "Mined #{proposals.size} skill proposal(s) for review."
39
+ rescue Llmemory::LLMError => e
40
+ redirect_to user_maintenance_path(user_id), alert: "Skill mining failed: #{e.message}"
41
+ end
42
+
43
+ # Registers a single reviewed proposal into procedural memory.
44
+ def register
45
+ user_id = params[:user_id]
46
+ procedural_memory(user_id).register_skill(
47
+ name: params[:name], body: params[:body], description: params[:description].presence,
48
+ kind: params[:kind].presence || Llmemory::LongTerm::Procedural::Skill::DEFAULT_KIND,
49
+ provenance: Llmemory::Provenance.build(method: "skill_mining")
50
+ )
51
+ redirect_to user_maintenance_path(user_id), notice: "Registered skill #{params[:name]}."
52
+ end
53
+
54
+ private
55
+
56
+ def episodic_memory(user_id)
57
+ Llmemory::LongTerm::Episodic::Memory.new(user_id: user_id, storage: episodic_storage)
58
+ end
59
+
60
+ def procedural_memory(user_id)
61
+ Llmemory::LongTerm::Procedural::Memory.new(user_id: user_id, storage: procedural_storage)
62
+ end
63
+
64
+ def build_semantic_memory(user_id)
65
+ if graph_based?
66
+ Llmemory::LongTerm::GraphBased::Memory.new(user_id: user_id, storage: graph_based_storage)
67
+ else
68
+ Llmemory::LongTerm::FileBased::Memory.new(user_id: user_id, storage: file_based_storage)
69
+ end
70
+ end
71
+
72
+ def pass_notice(report)
73
+ expired = report[:expired] || {}
74
+ msg = "Pass complete: #{Array(report[:insights]).size} insight(s), " \
75
+ "#{Array(report[:mined]).size} skill(s) mined, " \
76
+ "expired episodic=#{expired[:episodic] || 0} procedural=#{expired[:procedural] || 0}."
77
+ errors = report[:errors] || {}
78
+ errors.empty? ? msg : "#{msg} Errors: #{errors.map { |k, v| "#{k}: #{v}" }.join('; ')}"
79
+ end
80
+
81
+ # Proposals are stashed in the session so they survive the redirect to
82
+ # `show` without being persisted to a store before the user confirms.
83
+ def store_proposals(proposals)
84
+ session[:llmemory_skill_proposals] = proposals
85
+ end
86
+
87
+ def session_proposals
88
+ Array(session.delete(:llmemory_skill_proposals)).map { |p| p.respond_to?(:symbolize_keys) ? p.symbolize_keys : p }
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Dashboard
5
+ class ProceduralController < ApplicationController
6
+ def index
7
+ @user_id = params[:user_id]
8
+ @limit = (params[:limit].presence || 50).to_i
9
+ @offset = (params[:offset].presence || 0).to_i
10
+ @skills = procedural_storage.list_skills(@user_id, limit: @limit, offset: @offset)
11
+ @total = procedural_storage.count_skills(@user_id)
12
+ end
13
+
14
+ def forget
15
+ memory = Llmemory::LongTerm::Procedural::Memory.new(user_id: params[:user_id], storage: procedural_storage)
16
+ mode = params[:mode].to_s == "hard" ? :hard : :soft
17
+ memory.forget(ids: [params[:id]], reason: params[:reason], mode: mode)
18
+ redirect_to user_procedural_path(params[:user_id]), notice: "Forgot skill #{params[:id]} (#{mode})."
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Dashboard
5
+ # Read-only landing for reflection. Triggers (POST #run) build a Reflector
6
+ # over the configured episodic + semantic memories and run a single pass
7
+ # over the last N episodes.
8
+ class ReflectionController < ApplicationController
9
+ def show
10
+ @user_id = params[:user_id]
11
+ @recent_window = (params[:window].presence || 10).to_i
12
+ @recent_episodes = episodic_storage.list_episodes(@user_id, limit: @recent_window)
13
+ end
14
+
15
+ def run
16
+ user_id = params[:user_id]
17
+ window = (params[:window].presence || 10).to_i
18
+ episodic = Llmemory::LongTerm::Episodic::Memory.new(user_id: user_id, storage: episodic_storage)
19
+ semantic = build_semantic_memory(user_id)
20
+ insight_ids = Llmemory::Reflection::Reflector.new(episodic: episodic, semantic: semantic).reflect(window: window)
21
+ redirect_to user_reflection_path(user_id), notice: "Reflection produced #{insight_ids.size} insight(s)."
22
+ rescue Llmemory::LLMError => e
23
+ redirect_to user_reflection_path(user_id), alert: "Reflection failed: #{e.message}"
24
+ end
25
+
26
+ private
27
+
28
+ def build_semantic_memory(user_id)
29
+ if graph_based?
30
+ Llmemory::LongTerm::GraphBased::Memory.new(user_id: user_id, storage: graph_based_storage)
31
+ else
32
+ Llmemory::LongTerm::FileBased::Memory.new(user_id: user_id, storage: file_based_storage)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Dashboard
5
+ class WorkingController < ApplicationController
6
+ def show
7
+ @user_id = params[:user_id]
8
+ @session_id = params[:session_id]
9
+ @working = Llmemory::WorkingMemory.new(user_id: @user_id, session_id: @session_id, store: short_term_store)
10
+ @slots = @working.to_h
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ <div class="card">
2
+ <h2>Episodic memory: <%= @user_id %></h2>
3
+ <p>Total active episodes: <strong><%= @total %></strong></p>
4
+ <% if @episodes.blank? %>
5
+ <p class="empty">No episodes.</p>
6
+ <% else %>
7
+ <table>
8
+ <thead>
9
+ <tr><th>ID</th><th>Summary</th><th>Outcome</th><th>Steps</th><th>Created</th><th>Forget</th></tr>
10
+ </thead>
11
+ <tbody>
12
+ <% @episodes.each do |e| %>
13
+ <% id = e[:id] || e["id"] %>
14
+ <tr>
15
+ <td><%= id %></td>
16
+ <td><%= truncate((e[:summary] || e["summary"]).to_s, length: 80) %></td>
17
+ <td><%= e[:outcome] || e["outcome"] %></td>
18
+ <td><%= Array(e[:steps] || e["steps"]).size %></td>
19
+ <td><%= e[:created_at] || e["created_at"] %></td>
20
+ <td>
21
+ <%= button_to "Soft", forget_user_episodic_path(@user_id), params: { id: id, mode: "soft" }, method: :post %>
22
+ <%= button_to "Hard", forget_user_episodic_path(@user_id), params: { id: id, mode: "hard" }, method: :post, data: { confirm: "Hard-delete is irreversible." } %>
23
+ </td>
24
+ </tr>
25
+ <% end %>
26
+ </tbody>
27
+ </table>
28
+ <p>
29
+ <% if @offset.positive? %>
30
+ <a href="?offset=<%= [@offset - @limit, 0].max %>&limit=<%= @limit %>">&laquo; Newer</a>
31
+ <% end %>
32
+ <% if (@offset + @limit) < @total %>
33
+ <a href="?offset=<%= @offset + @limit %>&limit=<%= @limit %>">Older &raquo;</a>
34
+ <% end %>
35
+ </p>
36
+ <% end %>
37
+ </div>
@@ -0,0 +1,23 @@
1
+ <div class="card">
2
+ <h2>Forget log: <%= @user_id %></h2>
3
+ <p>Audit trail of removals (soft-archive + hard-delete). Recorded by <code>MemoryModule#forget</code>.</p>
4
+ <% if @entries.blank? %>
5
+ <p class="empty">No forget events.</p>
6
+ <% else %>
7
+ <table>
8
+ <thead>
9
+ <tr><th>Time</th><th>Memory type</th><th>Ids</th><th>Reason</th></tr>
10
+ </thead>
11
+ <tbody>
12
+ <% @entries.each do |entry| %>
13
+ <tr>
14
+ <td><%= entry[:archived_at] || entry["archived_at"] %></td>
15
+ <td><%= entry[:memory_type] || entry["memory_type"] %></td>
16
+ <td><code><%= Array(entry[:ids] || entry["ids"]).join(", ") %></code></td>
17
+ <td><%= entry[:reason] || entry["reason"] %></td>
18
+ </tr>
19
+ <% end %>
20
+ </tbody>
21
+ </table>
22
+ <% end %>
23
+ </div>
@@ -0,0 +1,65 @@
1
+ <div class="card">
2
+ <h2>Cognitive maintenance: <%= @user_id %></h2>
3
+ <p>The maintenance pass closes the CoALA learning loop: reflect (episodes &rarr; insights), mine skills (episodes &rarr; procedural), and expire entries past their TTL. Each step is isolated &mdash; a failure in one does not abort the others.</p>
4
+
5
+ <%= form_with url: run_user_maintenance_path(@user_id), method: :post, local: true do %>
6
+ <label>Reflection window (episodes): <%= number_field_tag :window, @window, min: 1, max: 100 %></label>
7
+ <label><%= check_box_tag :mine_skills, "1", true %> Mine &amp; register skills</label>
8
+ <%= submit_tag "Run maintenance pass" %>
9
+ <% end %>
10
+ </div>
11
+
12
+ <div class="card">
13
+ <h3>Review mined skills (human-in-the-loop)</h3>
14
+ <p>Mine proposals without registering them, then register the ones you want.</p>
15
+
16
+ <%= form_with url: mine_user_maintenance_path(@user_id), method: :post, local: true do %>
17
+ <label>Mining window (episodes): <%= number_field_tag :window, 20, min: 1, max: 100 %></label>
18
+ <%= submit_tag "Mine proposals" %>
19
+ <% end %>
20
+
21
+ <% if @proposals.present? %>
22
+ <table>
23
+ <thead>
24
+ <tr><th>Name</th><th>Kind</th><th>Confidence</th><th>Body</th><th>Register</th></tr>
25
+ </thead>
26
+ <tbody>
27
+ <% @proposals.each do |p| %>
28
+ <tr>
29
+ <td><%= p[:name] %></td>
30
+ <td><%= p[:kind] %></td>
31
+ <td><%= p[:confidence] %></td>
32
+ <td><%= truncate(p[:body].to_s, length: 120) %></td>
33
+ <td>
34
+ <%= button_to "Register", register_user_maintenance_path(@user_id), method: :post, params: {
35
+ name: p[:name], body: p[:body], kind: p[:kind], description: p[:description]
36
+ } %>
37
+ </td>
38
+ </tr>
39
+ <% end %>
40
+ </tbody>
41
+ </table>
42
+ <% end %>
43
+ </div>
44
+
45
+ <div class="card">
46
+ <h3>Last <%= @window %> episodes</h3>
47
+ <% if @recent_episodes.blank? %>
48
+ <p class="empty">No episodes to learn from.</p>
49
+ <% else %>
50
+ <table>
51
+ <thead>
52
+ <tr><th>ID</th><th>Summary</th><th>Outcome</th></tr>
53
+ </thead>
54
+ <tbody>
55
+ <% @recent_episodes.each do |e| %>
56
+ <tr>
57
+ <td><%= e[:id] || e["id"] %></td>
58
+ <td><%= truncate((e[:summary] || e["summary"]).to_s, length: 100) %></td>
59
+ <td><%= e[:outcome] || e["outcome"] %></td>
60
+ </tr>
61
+ <% end %>
62
+ </tbody>
63
+ </table>
64
+ <% end %>
65
+ </div>
@@ -0,0 +1,38 @@
1
+ <div class="card">
2
+ <h2>Procedural memory (skills): <%= @user_id %></h2>
3
+ <p>Total active skills: <strong><%= @total %></strong></p>
4
+ <% if @skills.blank? %>
5
+ <p class="empty">No skills.</p>
6
+ <% else %>
7
+ <table>
8
+ <thead>
9
+ <tr><th>ID</th><th>Name</th><th>Description</th><th>Success</th><th>Failure</th><th>Version</th><th>Forget</th></tr>
10
+ </thead>
11
+ <tbody>
12
+ <% @skills.each do |s| %>
13
+ <% id = s[:id] || s["id"] %>
14
+ <tr>
15
+ <td><%= id %></td>
16
+ <td><%= s[:name] || s["name"] %></td>
17
+ <td><%= truncate((s[:description] || s["description"]).to_s, length: 100) %></td>
18
+ <td><%= (s[:success_count] || s["success_count"]).to_i %></td>
19
+ <td><%= (s[:failure_count] || s["failure_count"]).to_i %></td>
20
+ <td><%= s[:version] || s["version"] %></td>
21
+ <td>
22
+ <%= button_to "Soft", forget_user_procedural_path(@user_id), params: { id: id, mode: "soft" }, method: :post %>
23
+ <%= button_to "Hard", forget_user_procedural_path(@user_id), params: { id: id, mode: "hard" }, method: :post, data: { confirm: "Hard-delete is irreversible." } %>
24
+ </td>
25
+ </tr>
26
+ <% end %>
27
+ </tbody>
28
+ </table>
29
+ <p>
30
+ <% if @offset.positive? %>
31
+ <a href="?offset=<%= [@offset - @limit, 0].max %>&limit=<%= @limit %>">&laquo; Newer</a>
32
+ <% end %>
33
+ <% if (@offset + @limit) < @total %>
34
+ <a href="?offset=<%= @offset + @limit %>&limit=<%= @limit %>">Older &raquo;</a>
35
+ <% end %>
36
+ </p>
37
+ <% end %>
38
+ </div>
@@ -0,0 +1,29 @@
1
+ <div class="card">
2
+ <h2>Reflection: <%= @user_id %></h2>
3
+ <p>The reflector distills the last N episodes into semantic insights (Reflexion / Generative Agents pattern), with provenance to the source episodes.</p>
4
+
5
+ <%= form_with url: run_user_reflection_path(@user_id), method: :post, local: true do %>
6
+ <label>Window (episodes): <%= number_field_tag :window, @recent_window, min: 1, max: 100 %></label>
7
+ <%= submit_tag "Run reflection" %>
8
+ <% end %>
9
+
10
+ <h3>Last <%= @recent_window %> episodes</h3>
11
+ <% if @recent_episodes.blank? %>
12
+ <p class="empty">No episodes to reflect over.</p>
13
+ <% else %>
14
+ <table>
15
+ <thead>
16
+ <tr><th>ID</th><th>Summary</th><th>Outcome</th></tr>
17
+ </thead>
18
+ <tbody>
19
+ <% @recent_episodes.each do |e| %>
20
+ <tr>
21
+ <td><%= e[:id] || e["id"] %></td>
22
+ <td><%= truncate((e[:summary] || e["summary"]).to_s, length: 100) %></td>
23
+ <td><%= e[:outcome] || e["outcome"] %></td>
24
+ </tr>
25
+ <% end %>
26
+ </tbody>
27
+ </table>
28
+ <% end %>
29
+ </div>
@@ -10,5 +10,21 @@
10
10
  <% end %>
11
11
  | <a href="<%= user_stats_path(@user_id) %>">Stats</a>
12
12
  </p>
13
+ <p>
14
+ <strong>Cognitive memory:</strong>
15
+ <a href="<%= user_episodic_path(@user_id) %>">Episodes</a>
16
+ | <a href="<%= user_procedural_path(@user_id) %>">Skills</a>
17
+ | <a href="<%= user_reflection_path(@user_id) %>">Reflection</a>
18
+ | <a href="<%= user_maintenance_path(@user_id) %>">Maintenance</a>
19
+ | <a href="<%= user_forget_log_path(@user_id) %>">Forget log</a>
20
+ </p>
13
21
  <p>Sessions: <%= @sessions.join(", ") %></p>
22
+ <% if @sessions.any? %>
23
+ <p>
24
+ <strong>Working memory:</strong>
25
+ <% @sessions.each do |s| %>
26
+ <a href="<%= user_working_path(@user_id, s) %>"><%= s %></a>
27
+ <% end %>
28
+ </p>
29
+ <% end %>
14
30
  </div>
@@ -0,0 +1,20 @@
1
+ <div class="card">
2
+ <h2>Working memory: <%= @user_id %> / <%= @session_id %></h2>
3
+ <% if @slots.blank? %>
4
+ <p class="empty">No slots set.</p>
5
+ <% else %>
6
+ <table>
7
+ <thead>
8
+ <tr><th>Slot</th><th>Value</th></tr>
9
+ </thead>
10
+ <tbody>
11
+ <% @slots.each do |slot, value| %>
12
+ <tr>
13
+ <td><code><%= slot %></code></td>
14
+ <td><pre><%= value.is_a?(String) ? value : value.inspect %></pre></td>
15
+ </tr>
16
+ <% end %>
17
+ </tbody>
18
+ </table>
19
+ <% end %>
20
+ </div>
data/config/routes.rb CHANGED
@@ -9,4 +9,18 @@ Llmemory::Dashboard::Engine.routes.draw do
9
9
  get "u/:user_id/graph", to: "graph#index", as: :user_graph
10
10
  get "u/:user_id/stats", to: "stats#index", as: :user_stats
11
11
  get "search", to: "search#index", as: :search
12
+
13
+ # Cognitive memory (CoALA) surface
14
+ get "u/:user_id/episodic", to: "episodic#index", as: :user_episodic
15
+ post "u/:user_id/episodic/forget", to: "episodic#forget", as: :forget_user_episodic
16
+ get "u/:user_id/procedural", to: "procedural#index", as: :user_procedural
17
+ post "u/:user_id/procedural/forget", to: "procedural#forget", as: :forget_user_procedural
18
+ get "u/:user_id/working/:session_id", to: "working#show", as: :user_working
19
+ get "u/:user_id/reflection", to: "reflection#show", as: :user_reflection
20
+ post "u/:user_id/reflection/run", to: "reflection#run", as: :run_user_reflection
21
+ get "u/:user_id/forget_log", to: "forget_log#show", as: :user_forget_log
22
+ get "u/:user_id/maintenance", to: "maintenance#show", as: :user_maintenance
23
+ post "u/:user_id/maintenance/run", to: "maintenance#run", as: :run_user_maintenance
24
+ post "u/:user_id/maintenance/mine", to: "maintenance#mine", as: :mine_user_maintenance
25
+ post "u/:user_id/maintenance/register", to: "maintenance#register", as: :register_user_maintenance
12
26
  end
@@ -30,6 +30,28 @@ class CreateLlmemoryTables < ActiveRecord::Migration[7.0]
30
30
  end
31
31
  add_index :llmemory_categories, [:user_id, :category_name], unique: true
32
32
 
33
+ # Episodic long-term memory (trajectories) — JSONB document per episode
34
+ create_table :llmemory_episodes, id: false do |t|
35
+ t.string :id, null: false, primary_key: true
36
+ t.string :user_id, null: false
37
+ t.jsonb :data, null: false, default: {}
38
+ t.text :search_text
39
+ t.datetime :archived_at
40
+ t.timestamps
41
+ end
42
+ add_index :llmemory_episodes, :user_id
43
+
44
+ # Procedural long-term memory (skill library) — JSONB document per skill
45
+ create_table :llmemory_skills, id: false do |t|
46
+ t.string :id, null: false, primary_key: true
47
+ t.string :user_id, null: false
48
+ t.jsonb :data, null: false, default: {}
49
+ t.text :search_text
50
+ t.datetime :archived_at
51
+ t.timestamps
52
+ end
53
+ add_index :llmemory_skills, :user_id
54
+
33
55
  create_table :llmemory_checkpoints do |t|
34
56
  t.string :user_id, null: false
35
57
  t.string :session_id, null: false
@@ -70,6 +70,14 @@ module Llmemory
70
70
  Llmemory::LongTerm::GraphBased::Storages::MemoryStorage.new
71
71
  end
72
72
  end
73
+
74
+ def episodic_storage(store_type = nil)
75
+ Llmemory::LongTerm::Episodic::Storages.build(store: (store_type || Llmemory.configuration.long_term_store))
76
+ end
77
+
78
+ def procedural_storage(store_type = nil)
79
+ Llmemory::LongTerm::Procedural::Storages.build(store: (store_type || Llmemory.configuration.long_term_store))
80
+ end
73
81
  end
74
82
  end
75
83
  end