context: summarize-on-evict callback + summary block (Phase 5 commit #2)
Phase 5 commit #2 per docs/PHASE5.md §3 / §6. Context.new opts additions: - summarize_fn(prior_summary, evicted_turns) -> string|nil callback per R-B1 canonical signature: (nil, [turns]) → first-time summarize (str, [turns]) → additive: extend prior summary (str, nil) → compress: re-summarize the prior nil return → silent eviction (Phase 0 behavior preserved) - max_summary_chars (default 2000) — when ctx.summary grows past this, the callback is invoked AGAIN with the compress signal so the summary stays bounded across long sessions Context.summary (string|nil) is the rolling summary state. Composed into the SYSTEM MESSAGE (not as a turns[] entry — A3 resolution avoids system/system back-to-back). compose_summary() emits: [earlier conversation summary] <ctx.summary> between [background] and the NORRIS suffix. Both [background] and [earlier summary] are SUPPRESSED when ctx.norris_active (R-C4 — mirrors R-C1 from Phase 4; planner stays focused on its goal). enforce_budget() rewrite: - Collects the evicted pair before removing. - Calls summarize_fn(self.summary, pair) under pcall — wraps any callback error so a broken summarizer can't crash the REPL. - Updates self.summary if callback returned non-empty string. - If new summary exceeds max_summary_chars, invokes compress pass (callback with evicted=nil). - Removes pair from turns (same final state as Phase 0). Context:reset() clears the summary alongside turns + pending_exec_output. Smoke-tested with a mock summarizer over a 10-turn context with max_turns=4 and max_summary_chars=80: - 6 turns evicted to bring count down to 4 - Callback fired 4 times (3 additive + 1 compress when summary crossed 80 chars) - to_messages includes [earlier conversation summary] block - Under norris_active=true, summary suppressed (block absent) - :reset clears ctx.summary Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+56
-8
@@ -45,6 +45,17 @@ function M.new(opts)
|
||||
-- Default true per PHASE2.md §12 "Q18 default"; flip from caller.
|
||||
use_tool_role = (opts.use_tool_role == nil) and true
|
||||
or opts.use_tool_role,
|
||||
-- Phase 5: summarize-on-evict. When set, enforce_budget calls
|
||||
-- summarize_fn(prior_summary, evicted_turns) -> string | nil
|
||||
-- and updates ctx.summary instead of silently dropping turns.
|
||||
-- Callback contract per PHASE5.md R-B1:
|
||||
-- (nil, [turns]) → first-time summarize
|
||||
-- (str, [turns]) → additive: extend prior summary with new turns
|
||||
-- (str, nil) → compress: re-summarize the prior summary
|
||||
-- Returns nil → fall back to silent eviction (Phase 0 behavior).
|
||||
summarize_fn = opts.summarize_fn,
|
||||
summary = nil, -- rolling summary string
|
||||
max_summary_chars = opts.max_summary_chars or 2000,
|
||||
}, Context)
|
||||
end
|
||||
|
||||
@@ -152,6 +163,14 @@ local function compose_background(items)
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
-- Phase 5 R-C4: summary block composer. Mirrors the [background]
|
||||
-- pattern; suppressed under Norris (callers already guard, but the
|
||||
-- function returns "" for empty input regardless).
|
||||
local function compose_summary(summary_text)
|
||||
if not summary_text or summary_text == "" then return "" end
|
||||
return "\n\n[earlier conversation summary]\n" .. summary_text
|
||||
end
|
||||
|
||||
-- Phase 3: NORRIS MODE suffix appended to the system prompt when
|
||||
-- self.norris_active. Carries self.norris_goal so eviction of the
|
||||
-- user's "[norris] goal: ..." turn doesn't lose the anchor.
|
||||
@@ -181,10 +200,13 @@ message if they declined.]]
|
||||
|
||||
function Context:to_messages()
|
||||
local sys_content = self.system_prompt
|
||||
-- Phase 4 [background] memory block. Suppressed during Norris (R-C1
|
||||
-- — avoid ~16K of redundant tokens per planning iteration).
|
||||
-- Phase 4 [background] memory block + Phase 5 [earlier summary]
|
||||
-- block. Both suppressed during Norris (R-C1 / R-C4 — avoid
|
||||
-- redundant tokens per planning iteration; planner stays focused
|
||||
-- on its goal anchor).
|
||||
if not self.norris_active then
|
||||
sys_content = sys_content .. compose_background(self.memory_items)
|
||||
sys_content = sys_content .. compose_summary(self.summary)
|
||||
end
|
||||
-- Phase 3 NORRIS MODE suffix. Last block so its instructions dominate.
|
||||
if self.norris_active and self.norris_goal then
|
||||
@@ -271,15 +293,40 @@ end
|
||||
function Context:enforce_budget()
|
||||
local evicted = 0
|
||||
while #self.turns > self.max_turns do
|
||||
table.remove(self.turns, 1)
|
||||
evicted = evicted + 1
|
||||
if #self.turns > self.max_turns or evicted % 2 == 1 then
|
||||
if #self.turns > 0 then
|
||||
table.remove(self.turns, 1)
|
||||
evicted = evicted + 1
|
||||
-- Collect evicted slice (pair: user + assistant)
|
||||
local pair = {}
|
||||
pair[#pair + 1] = self.turns[1]
|
||||
if #self.turns >= 2 then pair[#pair + 1] = self.turns[2] end
|
||||
|
||||
-- Phase 5: ask the summarize callback (if wired) to absorb this
|
||||
-- slice into the rolling summary. Callback contract per R-B1:
|
||||
-- summarize_fn(prior_summary, evicted_turns) -> string | nil
|
||||
-- nil return → silent eviction (Phase 0 behavior).
|
||||
if self.summarize_fn then
|
||||
local ok, new_summary = pcall(self.summarize_fn, self.summary, pair)
|
||||
if ok and type(new_summary) == "string" and new_summary ~= "" then
|
||||
self.summary = new_summary
|
||||
-- R-C1: if grown past cap, compress in a second pass.
|
||||
if #self.summary > self.max_summary_chars then
|
||||
local ok2, compressed = pcall(self.summarize_fn,
|
||||
self.summary, nil)
|
||||
if ok2 and type(compressed) == "string"
|
||||
and compressed ~= "" then
|
||||
self.summary = compressed
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove the pair from turns (matches Phase 0 visible effect)
|
||||
table.remove(self.turns, 1)
|
||||
evicted = evicted + 1
|
||||
if #self.turns > 0 and (#self.turns > self.max_turns
|
||||
or evicted % 2 == 1) then
|
||||
table.remove(self.turns, 1)
|
||||
evicted = evicted + 1
|
||||
end
|
||||
end
|
||||
return evicted
|
||||
end
|
||||
|
||||
@@ -296,6 +343,7 @@ end
|
||||
function Context:reset()
|
||||
self.turns = {}
|
||||
self.pending_exec_output = nil
|
||||
self.summary = nil
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user