repl: :memory summarize — LLM candidate extraction (Phase 4 commit #4)
Phase 4 commit #4 per docs/PHASE4.md §6. :memory summarize: 1. Source-of-truth: session log file via history.load(session_path), NOT ctx:to_messages() (R-C2). Skips turns tagged meta="summarize" so prior summarize exchanges don't self-amplify across multiple calls within the same session. 2. Pick summarizer model from cfg.memory.summarizer_model (default active model). 3. Build a transcript string ("role: content" per turn, 800 chars max per turn) and feed it as a single user turn alongside a system instruction asking for "(fact|pref|context): <content>" lines. 4. broker.chat with max_tokens=1024 + timeout_ms=90000 (the deep model can take a while; we don't want a 15s probe-cap here). 5. Log the response as an assistant turn with meta="summarize" so the next :memory summarize call filters it out. 6. Parse response lines tolerating markdown bullets and bold markup: ^%s*[-*]?%s*[*_]*(fact|pref|context)[*_]*:%s*(.+)$ 7. Per-candidate prompt: y / N / edit. y → memory:add(kind, content) edit → readline prompt for replacement text any other → drop 8. status: "summarize: added N / M candidates". Live-tested against hossenfelder/fast: Pipeline correct end-to-end. Model emitted one candidate; user confirmation prompt fired; item persisted; :memory list showed it. Candidate quality from the 1.5B model is poor — typical small-model behavior; deep/cloud models would do better but this isn't an aish bug. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ Meta commands:
|
|||||||
:memory forget <id> append a tombstone for <id>
|
:memory forget <id> append a tombstone for <id>
|
||||||
:memory clear forget all active items (confirms first)
|
:memory clear forget all active items (confirms first)
|
||||||
:memory inject reload memory.jsonl into ctx (after manual edits)
|
:memory inject reload memory.jsonl into ctx (after manual edits)
|
||||||
|
:memory summarize LLM-extract candidate items from this session
|
||||||
:help this message
|
:help this message
|
||||||
]]
|
]]
|
||||||
|
|
||||||
@@ -788,8 +789,101 @@ function M.run(config)
|
|||||||
inject_memory()
|
inject_memory()
|
||||||
renderer.status(("re-injected %d items"):format(
|
renderer.status(("re-injected %d items"):format(
|
||||||
(ctx.memory_items and #ctx.memory_items) or 0))
|
(ctx.memory_items and #ctx.memory_items) or 0))
|
||||||
|
elseif sub == "summarize" then
|
||||||
|
if not memory then renderer.status("memory unavailable"); return end
|
||||||
|
if not session_path then
|
||||||
|
renderer.status("no session log to summarize"); return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Source of truth is the session log file (R-C2).
|
||||||
|
-- Exclude prior summarize exchanges to avoid drift.
|
||||||
|
local turns, _meta = history.load(session_path)
|
||||||
|
if not turns or #turns == 0 then
|
||||||
|
renderer.status("session log empty; nothing to summarize"); return
|
||||||
|
end
|
||||||
|
local filtered = {}
|
||||||
|
for _, t in ipairs(turns) do
|
||||||
|
if t.meta ~= "summarize" then
|
||||||
|
filtered[#filtered + 1] =
|
||||||
|
("%s: %s"):format(t.role,
|
||||||
|
(t.content or ""):gsub("\n", " "):sub(1, 800))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local transcript = table.concat(filtered, "\n")
|
||||||
|
if #transcript < 50 then
|
||||||
|
renderer.status("session content too short to summarize"); return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pick summarizer model.
|
||||||
|
local sum_name = (config.memory and config.memory.summarizer_model)
|
||||||
|
or active_name
|
||||||
|
local sum_cfg = config.models[sum_name]
|
||||||
|
if not sum_cfg then
|
||||||
|
renderer.status("summarizer model not found: " .. sum_name); return
|
||||||
|
end
|
||||||
|
|
||||||
|
renderer.status(("summarizing via %s ..."):format(sum_name))
|
||||||
|
local reply, err = broker.chat(sum_cfg, {
|
||||||
|
{ role = "system", content =
|
||||||
|
"Read the following conversation transcript. Extract "
|
||||||
|
.. "facts, preferences, or context worth remembering "
|
||||||
|
.. "across future sessions. Output ONE candidate per "
|
||||||
|
.. "line, prefixed with the kind: \"fact: ...\", "
|
||||||
|
.. "\"pref: ...\", or \"context: ...\". Maximum 10 "
|
||||||
|
.. "candidates. No commentary outside candidate lines."
|
||||||
|
},
|
||||||
|
{ role = "user", content = transcript },
|
||||||
|
}, { max_tokens = 1024, timeout_ms = 90000 })
|
||||||
|
|
||||||
|
if not reply then
|
||||||
|
renderer.status("summarize failed: " .. tostring(err))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Persist the summarize-tagged assistant turn so future
|
||||||
|
-- :memory summarize filters it out (R-C2).
|
||||||
|
log_turn({ role = "assistant", content = reply, meta = "summarize" })
|
||||||
|
|
||||||
|
-- Parse candidates: tolerate bullets and bold markup.
|
||||||
|
local candidates = {}
|
||||||
|
for line in (reply .. "\n"):gmatch("([^\n]*)\n") do
|
||||||
|
local kind, body = line:match("^%s*[-*]?%s*[*_]*(%a+)[*_]*%s*:%s*(.+)$")
|
||||||
|
if kind then
|
||||||
|
kind = kind:lower()
|
||||||
|
if kind == "fact" or kind == "pref" or kind == "context" then
|
||||||
|
candidates[#candidates + 1] = { kind = kind,
|
||||||
|
content = body:gsub("%s+$", "") }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #candidates == 0 then
|
||||||
|
renderer.status("no candidates parsed from response"); return
|
||||||
|
end
|
||||||
|
|
||||||
|
local added = 0
|
||||||
|
for _, cand in ipairs(candidates) do
|
||||||
|
io.write(("\n[memory] candidate (%s): %s\n")
|
||||||
|
:format(cand.kind, cand.content))
|
||||||
|
local ans = rl.readline("keep? [y/N/edit] ") or ""
|
||||||
|
local first = ans:lower():sub(1, 1)
|
||||||
|
if first == "y" then
|
||||||
|
memory:add(cand.kind, cand.content)
|
||||||
|
added = added + 1
|
||||||
|
elseif first == "e" then
|
||||||
|
local edited = rl.readline("edit: ") or ""
|
||||||
|
edited = edited:gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
if edited ~= "" then
|
||||||
|
memory:add(cand.kind, edited)
|
||||||
|
added = added + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
inject_memory()
|
||||||
|
renderer.status(("summarize: added %d / %d candidates")
|
||||||
|
:format(added, #candidates))
|
||||||
else
|
else
|
||||||
renderer.status("usage: :memory {list|add|forget|clear|inject}")
|
renderer.status("usage: :memory {list|add|forget|clear|inject|summarize}")
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
safety = function(args)
|
safety = function(args)
|
||||||
|
|||||||
Reference in New Issue
Block a user