Class: Kward::Memory::Manager
- Inherits:
-
Object
- Object
- Kward::Memory::Manager
- Defined in:
- lib/kward/memory/manager.rb
Overview
Manages Kward's opt-in structured memory store.
Core memories are explicit, high-trust instructions. Soft memories are confidence-scored contextual hints. Session memory is handled by session persistence, while this manager owns global/workspace JSON and JSONL files plus the audit event log.
Constant Summary collapse
- CORE_LIMIT =
6- SOFT_LIMIT =
6- DEFAULT_SOFT_TTL_DAYS =
60- DEFAULT_SOFT_CONFIDENCE =
0.65- EMOTIONAL_PATTERN =
/\b(love|loves|romantic|intimate|dependency|depend on me|need me|flirty|crush)\b/i- MEMORY_DUPLICATE_STOPWORDS =
Set.new(%w[a an and as at for in of on or that the this to with])
Instance Attribute Summary collapse
-
#last_retrieval ⇒ Object
readonly
Details for the most recent retrieval, used by
/memory why.
Class Method Summary collapse
Instance Method Summary collapse
-
#add_core(text, scope: "global", tags: [], source: "explicit_user_instruction", pinned: true) ⇒ Hash
Stores an explicit high-trust memory.
-
#add_soft(text, scope: "global", tags: [], confidence: DEFAULT_SOFT_CONFIDENCE, ttl_days: DEFAULT_SOFT_TTL_DAYS, source: "manual") ⇒ Hash
Stores a contextual hint with confidence and optional expiry.
- #auto_summary_disable ⇒ Object
- #auto_summary_enable ⇒ Object
- #auto_summary_enabled=(value) ⇒ Object
- #auto_summary_enabled? ⇒ Boolean
- #deduplicate_soft_records(records) ⇒ Object
- #default_client ⇒ Object
- #disable ⇒ Object
- #duplicate_memory_text?(text, existing_texts) ⇒ Boolean
- #enable ⇒ Object
-
#enabled? ⇒ Boolean
Whether memory prompt injection is enabled.
-
#explain_retrieval ⇒ Hash
Explanation data for the most recent retrieval.
- #forget_memory(id) ⇒ Object
- #hierarchy(workspace_root: Dir.pwd, include_inactive: false) ⇒ Object
- #infer_soft_from_text(text, workspace_root: Dir.pwd, client: nil, existing_texts: []) ⇒ Object
-
#initialize(config_path: ConfigFiles.config_path, core_path: ConfigFiles.memory_core_path, soft_path: ConfigFiles.memory_soft_path, events_path: ConfigFiles.memory_events_path, now: nil) ⇒ Manager
constructor
Creates an object for memory storage and retrieval.
- #inspect_memory ⇒ Object
- #list(include_inactive: false) ⇒ Object
- #llm_summarize(client, text) ⇒ Object
-
#memory_block(retrieval) ⇒ Object
Formats retrieved memories for system prompt injection.
- #memory_duplicate_key(text) ⇒ Object
- #memory_duplicate_token(term) ⇒ Object
- #memory_duplicate_tokens(text) ⇒ Object
- #normalize_inferred_memory_text(text, source_text: nil) ⇒ Object
- #paths ⇒ Object
- #promote_memory(id) ⇒ Object
- #promote_soft_to_core(id) ⇒ Object
- #relax_core(id, workspace_root: Dir.pwd) ⇒ Object
-
#retrieve_relevant(input:, workspace_root: Dir.pwd, max_core: CORE_LIMIT, max_soft: SOFT_LIMIT) ⇒ Hash
Selects bounded core and soft memories relevant to an interactive turn.
- #set_auto_summary(value) ⇒ Object
- #should_use_llm_summarization? ⇒ Boolean
-
#summarize_conversation(conversation, client: nil) ⇒ Object
Infers bounded session/workspace soft memories from a conversation.
- #summarize_text(text, client: nil) ⇒ Object
- #third_person_verb(verb) ⇒ Object
Constructor Details
#initialize(config_path: ConfigFiles.config_path, core_path: ConfigFiles.memory_core_path, soft_path: ConfigFiles.memory_soft_path, events_path: ConfigFiles.memory_events_path, now: nil) ⇒ Manager
Creates an object for memory storage and retrieval.
42 43 44 45 46 47 48 49 |
# File 'lib/kward/memory/manager.rb', line 42 def initialize(config_path: ConfigFiles.config_path, core_path: ConfigFiles.memory_core_path, soft_path: ConfigFiles.memory_soft_path, events_path: ConfigFiles.memory_events_path, now: nil) @config_path = config_path @core_path = core_path @soft_path = soft_path @events_path = events_path @now = now @last_retrieval = nil end |
Instance Attribute Details
#last_retrieval ⇒ Object (readonly)
Details for the most recent retrieval, used by /memory why.
29 30 31 |
# File 'lib/kward/memory/manager.rb', line 29 def last_retrieval @last_retrieval end |
Class Method Details
.for_config_dir(config_dir, now: nil) ⇒ Object
31 32 33 34 35 36 37 38 39 |
# File 'lib/kward/memory/manager.rb', line 31 def self.for_config_dir(config_dir, now: nil) new( config_path: File.join(config_dir, "config.json"), core_path: File.join(config_dir, "memory", "core.json"), soft_path: File.join(config_dir, "memory", "soft.jsonl"), events_path: File.join(config_dir, "memory", "events.jsonl"), now: now ) end |
Instance Method Details
#add_core(text, scope: "global", tags: [], source: "explicit_user_instruction", pinned: true) ⇒ Hash
Stores an explicit high-trust memory.
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/kward/memory/manager.rb', line 123 def add_core(text, scope: "global", tags: [], source: "explicit_user_instruction", pinned: true) record = { "id" => next_id("core", core_memories.map { |item| item["id"] }), "text" => clean_text(normalize_memory_text(text)), "scope" => clean_scope(scope), "tags" => (), "created_at" => , "updated_at" => , "source" => source, "pinned" => pinned ? true : false } raise ArgumentError, "Memory text cannot be empty" if record["text"].empty? memories = core_memories memories << record write_core(memories) append_event("add", event_ref(record, layer: "core")) record end |
#add_soft(text, scope: "global", tags: [], confidence: DEFAULT_SOFT_CONFIDENCE, ttl_days: DEFAULT_SOFT_TTL_DAYS, source: "manual") ⇒ Hash
Stores a contextual hint with confidence and optional expiry.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/kward/memory/manager.rb', line 151 def add_soft(text, scope: "global", tags: [], confidence: DEFAULT_SOFT_CONFIDENCE, ttl_days: DEFAULT_SOFT_TTL_DAYS, source: "manual") normalized_scope = clean_scope(scope) normalized_text = clean_text(normalize_memory_text(text)) raise ArgumentError, "Memory text cannot be empty" if normalized_text.empty? raise ArgumentError, "Refusing to persist emotional or dependency-forming memory automatically" if source == "inferred" && unsafe_soft_text?(normalized_text) existing = soft_memories.find do |item| item["scope"] == normalized_scope && duplicate_memory_text?(normalized_text, [item["text"]]) end return existing if existing record = { "id" => next_id("soft", soft_memories(include_inactive: true).map { |item| item["id"] }), "text" => normalized_text, "scope" => normalized_scope, "tags" => (), "confidence" => [[confidence.to_f, 0.0].max, 1.0].min, "hits" => 0, "created_at" => , "updated_at" => , "last_seen_at" => , "ttl_days" => ttl_days.to_i <= 0 ? DEFAULT_SOFT_TTL_DAYS : ttl_days.to_i, "source" => source, "status" => "active" } append_soft(record) append_event("add", event_ref(record, layer: "soft")) record end |
#auto_summary_disable ⇒ Object
113 114 115 |
# File 'lib/kward/memory/manager.rb', line 113 def auto_summary_disable set_auto_summary(false) end |
#auto_summary_enable ⇒ Object
109 110 111 |
# File 'lib/kward/memory/manager.rb', line 109 def auto_summary_enable set_auto_summary(true) end |
#auto_summary_enabled=(value) ⇒ Object
89 90 91 |
# File 'lib/kward/memory/manager.rb', line 89 def auto_summary_enabled=(value) set_auto_summary(value) end |
#auto_summary_enabled? ⇒ Boolean
81 82 83 84 85 86 87 |
# File 'lib/kward/memory/manager.rb', line 81 def auto_summary_enabled? config = ConfigFiles.read_config(@config_path) memory = config["memory"] memory.is_a?(Hash) && memory["auto_summary"] == true rescue StandardError false end |
#deduplicate_soft_records(records) ⇒ Object
469 470 471 472 473 474 475 476 477 478 479 480 481 482 |
# File 'lib/kward/memory/manager.rb', line 469 def deduplicate_soft_records(records) seen = [] records.each_with_object([]) do |record, deduplicated| scope = record["scope"] text = record["text"] duplicate = seen.any? do |seen_record| seen_record["scope"] == scope && duplicate_memory_text?(text, [seen_record["text"]]) end next if duplicate seen << record deduplicated << record end end |
#default_client ⇒ Object
511 512 513 514 515 |
# File 'lib/kward/memory/manager.rb', line 511 def default_client @default_client ||= Kward::Client.new rescue StandardError nil end |
#disable ⇒ Object
71 72 73 74 75 76 77 78 79 |
# File 'lib/kward/memory/manager.rb', line 71 def disable config = ConfigFiles.read_config(@config_path) memory = config["memory"].is_a?(Hash) ? config["memory"].dup : {} memory["enabled"] = false config["memory"] = memory ConfigFiles.write_config(config, @config_path) append_event("disable", {}) true end |
#duplicate_memory_text?(text, existing_texts) ⇒ Boolean
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 |
# File 'lib/kward/memory/manager.rb', line 420 def duplicate_memory_text?(text, existing_texts) candidate = memory_duplicate_key(text) candidate_tokens = memory_duplicate_tokens(text) Array(existing_texts).any? do |existing| existing_key = memory_duplicate_key(existing) next true if existing_key == candidate existing_tokens = memory_duplicate_tokens(existing) next false if candidate_tokens.empty? || existing_tokens.empty? overlap = (candidate_tokens & existing_tokens).length union = (candidate_tokens | existing_tokens).length union.positive? && overlap.to_f / union >= 0.8 end end |
#enable ⇒ Object
60 61 62 63 64 65 66 67 68 69 |
# File 'lib/kward/memory/manager.rb', line 60 def enable config = ConfigFiles.read_config(@config_path) memory = config["memory"].is_a?(Hash) ? config["memory"].dup : {} memory["enabled"] = true config["memory"] = memory ConfigFiles.write_config(config, @config_path) ensure_storage! append_event("enable", {}) true end |
#enabled? ⇒ Boolean
Returns whether memory prompt injection is enabled.
52 53 54 55 56 57 58 |
# File 'lib/kward/memory/manager.rb', line 52 def enabled? config = ConfigFiles.read_config(@config_path) memory = config["memory"] memory.is_a?(Hash) && memory["enabled"] == true rescue StandardError false end |
#explain_retrieval ⇒ Hash
Returns explanation data for the most recent retrieval.
313 314 315 |
# File 'lib/kward/memory/manager.rb', line 313 def explain_retrieval @last_retrieval || { "enabled" => enabled?, "core" => [], "soft" => [], "reasons" => [], "message" => "No memory retrieval has run yet." } end |
#forget_memory(id) ⇒ Object
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/kward/memory/manager.rb', line 214 def forget_memory(id) id = id.to_s memories = core_memories if memories.any? { |item| item["id"] == id } write_core(memories.reject { |item| item["id"] == id }) append_event("forget", { "id" => id, "layer" => "core" }) return true end records = soft_memories(include_inactive: true) found = false records.each do |item| next unless item["id"] == id && item["status"] != "forgotten" item["text"] = "[forgotten]" item["tags"] = [] item["confidence"] = 0.0 item["hits"] = 0 item["status"] = "forgotten" item["updated_at"] = found = true end if found write_soft(records) append_event("forget", { "id" => id, "layer" => "soft" }) end found end |
#hierarchy(workspace_root: Dir.pwd, include_inactive: false) ⇒ Object
249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/kward/memory/manager.rb', line 249 def hierarchy(workspace_root: Dir.pwd, include_inactive: false) workspace = workspace_scope(workspace_root) core = core_memories soft = soft_memories(include_inactive: include_inactive) soft = deduplicate_soft_records(soft) unless include_inactive { "global_core" => core.select { |item| item["scope"] == "global" }, "workspace_core" => core.select { |item| item["scope"] == workspace }, "workspace_soft" => soft.select { |item| item["scope"] == workspace } } end |
#infer_soft_from_text(text, workspace_root: Dir.pwd, client: nil, existing_texts: []) ⇒ Object
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 |
# File 'lib/kward/memory/manager.rb', line 366 def infer_soft_from_text(text, workspace_root: Dir.pwd, client: nil, existing_texts: []) candidates = heuristic_candidates(text) existing_set = Set.new(existing_texts.map { |t| normalize_for_comparison(t) }) scope = workspace_scope(workspace_root) workspace_scopes = [scope, clean_scope("workspace:#{workspace_root}")].uniq workspace_soft_texts = soft_memories.select { |memory| workspace_scopes.include?(memory["scope"]) }.map { |memory| memory["text"] } candidates.filter_map do |candidate| summarized = normalize_inferred_memory_text(summarize_text(candidate, client: client), source_text: candidate) normalized = normalize_for_comparison(summarized) # Skip if this text already exists in provided list or existing soft memories next if existing_set.include?(normalized) next if duplicate_memory_text?(summarized, existing_texts) next if duplicate_memory_text?(summarized, workspace_soft_texts) record = add_soft(summarized, scope: scope, tags: ["workflow"], confidence: 0.55, source: "inferred") workspace_soft_texts << record["text"] record end end |
#inspect_memory ⇒ Object
261 262 263 |
# File 'lib/kward/memory/manager.rb', line 261 def inspect_memory list(include_inactive: true).merge("enabled" => enabled?, "paths" => paths) end |
#list(include_inactive: false) ⇒ Object
243 244 245 246 247 |
# File 'lib/kward/memory/manager.rb', line 243 def list(include_inactive: false) soft = soft_memories(include_inactive: include_inactive) soft = deduplicate_soft_records(soft) unless include_inactive { "core" => core_memories, "soft" => soft } end |
#llm_summarize(client, text) ⇒ Object
517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 |
# File 'lib/kward/memory/manager.rb', line 517 def llm_summarize(client, text) = [ { role: "system", content: <<~SYSTEM }, You are a memory text reformulation assistant. Your task is to transform user-generated text into proper third-person descriptive memory statements. Rules: - Convert first-person statements ("I like", "we prefer") to third-person ("The user likes", "The user prefers") - Remove conversational filler, preambles, and action descriptions - Keep only the factual preference or fact - Always use "The user" as the subject for personal preferences - Do not use persona-specific names, titles, roles, or nicknames - Preserve workflow-related technical preferences as-is - Keep the text concise (under 100 characters) - If the text is already a good memory statement, return it unchanged Examples: - "I like to eat the most important meal today: steak" → "The user likes eating steak" - "We should prefer TDD for this project" → "Prefer TDD for this project" - "Remember that the user prefers minitest" → "The user prefers minitest" - "But first we need to remember that we are using TDD" → "Use TDD" - "The user usually asks for tests" → "The user usually asks for tests" - "Prefer evidence from code, tests, logs" → "Prefer evidence from code, tests, logs" SYSTEM { role: "user", content: "Reformulate this as a memory statement: #{text}" } ] response = client.chat(, max_tokens: 100, reasoning: false) content = response.dig("content") || response[:content] return text unless content content.strip rescue StandardError text end |
#memory_block(retrieval) ⇒ Object
Formats retrieved memories for system prompt injection.
Keep this block compact and explicit: it is read by the model, shown in
transcripts, and explained by /memory why. Do not include inactive or
forgotten memories here; retrieval already decides the active set.
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 |
# File 'lib/kward/memory/manager.rb', line 322 def memory_block(retrieval) core = Array(retrieval["core"]) soft = Array(retrieval["soft"]) return nil if core.empty? && soft.empty? global_core = core.select { |item| item["scope"] == "global" } workspace_core = core.reject { |item| item["scope"] == "global" } lines = ["<kward_memory>"] unless global_core.empty? lines << "Global Core Memories:" global_core.each { |item| lines << "- [#{item["id"]}] #{item["text"]}" } lines << "" end unless workspace_core.empty? lines << "Workspace Core Memories:" workspace_core.each { |item| lines << "- [#{item["id"]}] #{item["text"]}" } lines << "" end unless soft.empty? lines << "Workspace Soft Memories:" soft.each { |item| lines << "- [#{item["id"]}] #{item["text"]}" } lines << "" end lines << "Rules:" lines << "- Core memories override soft memories." lines << "- Soft memories are contextual hints, not guaranteed facts." lines << "</kward_memory>" lines.join("\n") end |
#memory_duplicate_key(text) ⇒ Object
436 437 438 439 440 441 442 443 444 445 446 447 |
# File 'lib/kward/memory/manager.rb', line 436 def memory_duplicate_key(text) normalized = normalize_for_comparison(text).downcase normalized = normalized.sub(/\Ai\s+(?:(usually|always)\s+)?(prefer|like|use|want|believe|think|avoid)\b/i) do ["the user", Regexp.last_match(1)&.downcase, third_person_verb(Regexp.last_match(2))].compact.join(" ") end normalized = normalized.sub(/\Awe\s+(?:(usually|always)\s+)?(prefer|like|use|want|believe|think|avoid)\b/i) do ["the user", Regexp.last_match(1)&.downcase, third_person_verb(Regexp.last_match(2))].compact.join(" ") end normalized = normalized.sub(/\Amy\s+/i, "the user's ") normalized = normalized.sub(/\Aour\s+/i, "the user's ") normalized.tr("“”‘’", "\"\"''") end |
#memory_duplicate_token(term) ⇒ Object
456 457 458 459 460 461 462 463 464 465 466 467 |
# File 'lib/kward/memory/manager.rb', line 456 def memory_duplicate_token(term) case term when "uses", "using" "use" when "prefers", "preferring" "prefer" when "avoids", "avoiding" "avoid" else term.sub(/\A(.{4,})s\z/, '\\1') end end |
#memory_duplicate_tokens(text) ⇒ Object
449 450 451 452 453 454 |
# File 'lib/kward/memory/manager.rb', line 449 def memory_duplicate_tokens(text) memory_duplicate_key(text).scan(/[a-z0-9_\-']{3,}/).filter_map do |term| token = memory_duplicate_token(term) token unless MEMORY_DUPLICATE_STOPWORDS.include?(token) end.uniq end |
#normalize_inferred_memory_text(text, source_text: nil) ⇒ Object
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 |
# File 'lib/kward/memory/manager.rb', line 386 def normalize_inferred_memory_text(text, source_text: nil) normalized = normalize_memory_text(text) source = source_text.to_s first_person_source = source.match?(/\b(?:i|we|my|our)\b/i) if first_person_source normalized = normalized.sub(/\AThe\s+\w+\s+(prefers|likes|uses|usually|always|wants|believes|thinks|avoids)\b/i) do "The user #{Regexp.last_match(1).downcase}" end normalized = normalized.sub(/\AI\s+(?:(usually|always)\s+)?(prefer|like|use|want|believe|think|avoid)\b/i) do adverb = Regexp.last_match(1) verb = third_person_verb(Regexp.last_match(2)) ["The user", adverb&.downcase, verb].compact.join(" ") end normalized = normalized.sub(/\AWe\s+(?:(usually|always)\s+)?(prefer|like|use|want|believe|think|avoid)\b/i) do adverb = Regexp.last_match(1) verb = third_person_verb(Regexp.last_match(2)) ["The user", adverb&.downcase, verb].compact.join(" ") end normalized = normalized.sub(/\AMy\s+/i, "The user's ") normalized = normalized.sub(/\AOur\s+/i, "The user's ") end clean_text(normalized) end |
#paths ⇒ Object
552 553 554 |
# File 'lib/kward/memory/manager.rb', line 552 def paths { "core" => @core_path, "soft" => @soft_path, "events" => @events_path } end |
#promote_memory(id) ⇒ Object
182 183 184 185 186 187 188 |
# File 'lib/kward/memory/manager.rb', line 182 def promote_memory(id) id = id.to_s core = core_memories.find { |item| item["id"] == id } return promote_core_to_global(core) if core promote_soft_to_core(id) end |
#promote_soft_to_core(id) ⇒ Object
190 191 192 193 194 195 196 197 198 |
# File 'lib/kward/memory/manager.rb', line 190 def promote_soft_to_core(id) soft = soft_memories.find { |item| item["id"] == id.to_s } raise ArgumentError, "Unknown active soft memory or workspace core memory: #{id}" unless soft core = add_core(soft["text"], scope: soft["scope"], tags: soft["tags"], source: "promoted_soft_memory", pinned: true) forget_memory(id) append_event("promote", { "from_id" => soft["id"], "to_id" => core["id"] }) core end |
#relax_core(id, workspace_root: Dir.pwd) ⇒ Object
200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/kward/memory/manager.rb', line 200 def relax_core(id, workspace_root: Dir.pwd) id = id.to_s memories = core_memories core = memories.find { |item| item["id"] == id } raise ArgumentError, "Unknown core memory: #{id}" unless core raise ArgumentError, "Only global core memories can be relaxed" unless core["scope"] == "global" core["scope"] = workspace_scope(workspace_root) core["updated_at"] = write_core(memories) append_event("relax", event_ref(core, layer: "core")) core end |
#retrieve_relevant(input:, workspace_root: Dir.pwd, max_core: CORE_LIMIT, max_soft: SOFT_LIMIT) ⇒ Hash
Selects bounded core and soft memories relevant to an interactive turn.
Retrieval is scoped to global and workspace memories, prefers core memories, and scores soft memories by text/tag overlap, confidence, and expiry.
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
# File 'lib/kward/memory/manager.rb', line 274 def retrieve_relevant(input:, workspace_root: Dir.pwd, max_core: CORE_LIMIT, max_soft: SOFT_LIMIT) unless enabled? @last_retrieval = { "enabled" => false, "core" => [], "soft" => [], "reasons" => [] } return @last_retrieval end scopes = scopes_for(workspace_root) workspace = workspace_scope(workspace_root) terms = terms_for(input) core = ranked_core_memories(workspace).first(max_core) core_reasons = core.map { |item| reason_for(item, layer: "core", score: 1.0, reasons: ["scope match", "core memories are preferred"]) } soft_records_all = soft_memories(include_inactive: true) soft_scored = deduplicate_soft_records(soft_records_all.select { |item| item["status"] == "active" }).filter_map do |item| next unless item["scope"] == workspace next if expired?(item) score, reasons = soft_score(item, terms) next if score <= 0 [item, score, reasons] end soft = soft_scored.sort_by { |item, score, _reasons| [-score, -item["confidence"].to_f, item["id"].to_s] }.first(max_soft) soft_records = soft.map(&:first) touch_soft_records(soft_records_all, soft_records) soft_reasons = soft.map { |item, score, reasons| reason_for(item, layer: "soft", score: score, reasons: reasons) } @last_retrieval = { "enabled" => true, "scopes" => scopes, "core" => core, "soft" => soft_records, "reasons" => core_reasons + soft_reasons } append_event("retrieve", { "core_ids" => core.map { |item| item["id"] }, "soft_ids" => soft_records.map { |item| item["id"] }, "scopes" => scopes }) @last_retrieval end |
#set_auto_summary(value) ⇒ Object
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/kward/memory/manager.rb', line 93 def set_auto_summary(value) config = ConfigFiles.read_config(@config_path) memory = config["memory"].is_a?(Hash) ? config["memory"].dup : {} enabled = value ? true : false previous = memory["auto_summary"] == true if enabled memory["auto_summary"] = true else memory.delete("auto_summary") end config["memory"] = memory ConfigFiles.write_config(config, @config_path) append_event(enabled ? "auto_summary_enable" : "auto_summary_disable", {}) unless previous == enabled auto_summary_enabled? end |
#should_use_llm_summarization? ⇒ Boolean
498 499 500 501 502 503 504 505 506 507 508 509 |
# File 'lib/kward/memory/manager.rb', line 498 def should_use_llm_summarization? # Check if we have valid credentials to make LLM calls client = default_client return false unless client token = client.instance_variable_get(:@openai_access_token) || client.instance_variable_get(:@openrouter_api_key) || client.send(:github_access_token) token.to_s.length > 10 rescue StandardError false end |
#summarize_conversation(conversation, client: nil) ⇒ Object
Infers bounded session/workspace soft memories from a conversation.
This is best-effort and intentionally conservative. It may use an LLM when configured, but failures fall back to heuristic text or no-op behavior so memory summarization never blocks normal session flow.
358 359 360 361 362 363 364 |
# File 'lib/kward/memory/manager.rb', line 358 def summarize_conversation(conversation, client: nil) text = (conversation).map { || MessageAccess.content() }.compact.join("\n") existing_texts = Array(conversation.session_memories).map { |memory| memory["text"] } records = infer_soft_from_text(text, workspace_root: conversation.workspace_root, client: client, existing_texts: existing_texts) conversation.session_memories.concat(records.map { |record| record.slice("id", "text", "scope", "tags") }) if conversation.session_memories.respond_to?(:concat) records end |
#summarize_text(text, client: nil) ⇒ Object
484 485 486 487 488 489 490 491 492 493 494 495 496 |
# File 'lib/kward/memory/manager.rb', line 484 def summarize_text(text, client: nil) summarizer_client = client unless summarizer_client return text unless should_use_llm_summarization? summarizer_client = default_client end summary = llm_summarize(summarizer_client, text) summary.empty? ? text : summary rescue StandardError text end |
#third_person_verb(verb) ⇒ Object
412 413 414 415 416 417 418 |
# File 'lib/kward/memory/manager.rb', line 412 def third_person_verb(verb) word = verb.to_s.downcase return word if ["usually", "always"].include?(word) return "uses" if word == "use" word.end_with?("s") ? word : "#{word}s" end |