context: in-memory turn list + max_turns sliding-window eviction
Phase 0 implementation per PHASE0.md §6, §8.
Context.new(opts) constructs with the §6 default system prompt (the
`CMD: ` extraction contract is hard-coded in there per §3 — locked
substrate, do not edit). opts overrides: system_prompt, max_turns
(default 40), token_budget (default 4096; visibility only in Phase 0
per Q1, deferred to Phase 3 for accurate tokenization).
API:
ctx:append({role, content}) record a turn
ctx:to_messages() [{system,...}, ...turns] for broker.chat
ctx:enforce_budget() evict pairs (user+assistant) until
#turns <= max_turns; returns count
ctx:estimate_tokens() char/4 heuristic
ctx:reset() drop all turns (system_prompt kept)
System prompt is the §6 phrasing verbatim including the `CMD: ` clause
— stored on the context, NOT in self.turns, so it is prepended freshly
on every to_messages() call.
Smoke covers basic ops, no-evict-at-max, evict-on-overflow, bulk
eviction (14 turns -> 4), reset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+63
-14
@@ -1,28 +1,77 @@
|
||||
-- context.lua — in-memory conversation history + token budget.
|
||||
-- Phase 0: ordered turn list, sliding window eviction.
|
||||
-- Tokenization is char/4 heuristic in Phase 0; accurate count is Phase 2.
|
||||
-- See docs/PHASE0.md §8.
|
||||
-- Phase 0: ordered turn list, sliding-window eviction by max_turns.
|
||||
-- Tokenization is char/4 heuristic in Phase 0; accurate count is Phase 3 (Q1).
|
||||
-- See docs/PHASE0.md §6, §8.
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Construct a Context table from config.context.
|
||||
-- The §6 default system prompt. The `CMD: ` (exact prefix, single space)
|
||||
-- contract is locked per §3 invariants — do not edit without amending PHASE0.
|
||||
local DEFAULT_SYSTEM_PROMPT = [[
|
||||
You are aish, an AI-augmented shell assistant. You help the user execute shell
|
||||
commands, write and debug code, and re-engineer software. When suggesting shell
|
||||
commands, output them on a line beginning with exactly "CMD: " so aish can
|
||||
identify and optionally execute them. Be concise. Prefer concrete actions over
|
||||
explanations unless asked.]]
|
||||
|
||||
local Context = {}
|
||||
Context.__index = Context
|
||||
|
||||
function M.new(opts)
|
||||
error("context.new: not implemented (Phase 0 pending)")
|
||||
opts = opts or {}
|
||||
return setmetatable({
|
||||
system_prompt = opts.system_prompt or DEFAULT_SYSTEM_PROMPT,
|
||||
turns = {},
|
||||
max_turns = opts.max_turns or 40,
|
||||
token_budget = opts.token_budget or 4096,
|
||||
}, Context)
|
||||
end
|
||||
|
||||
-- Append a turn { role = ..., content = ... }.
|
||||
function M:append(turn)
|
||||
error("context:append: not implemented (Phase 0 pending)")
|
||||
function Context:append(turn)
|
||||
assert(type(turn) == "table" and turn.role and turn.content,
|
||||
"context:append requires { role = ..., content = ... }")
|
||||
self.turns[#self.turns + 1] = { role = turn.role, content = turn.content }
|
||||
end
|
||||
|
||||
-- Render messages array suitable for broker.chat (system prompt prepended).
|
||||
function M:to_messages()
|
||||
error("context:to_messages: not implemented (Phase 0 pending)")
|
||||
-- Render the messages array for broker.chat (system prompt prepended; turns
|
||||
-- in order). The system prompt is NOT stored in self.turns per §6.
|
||||
function Context:to_messages()
|
||||
local msgs = { { role = "system", content = self.system_prompt } }
|
||||
for _, t in ipairs(self.turns) do
|
||||
msgs[#msgs + 1] = { role = t.role, content = t.content }
|
||||
end
|
||||
return msgs
|
||||
end
|
||||
|
||||
-- Apply max_turns eviction policy. Returns number of turns evicted.
|
||||
function M:enforce_budget()
|
||||
error("context:enforce_budget: not implemented (Phase 0 pending)")
|
||||
-- Evict the oldest pair (user + assistant) while we exceed max_turns. Returns
|
||||
-- total turns evicted. Caller is responsible for rendering the §8 status line.
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
return evicted
|
||||
end
|
||||
|
||||
-- Coarse char/4 token estimate per §8. Phase 0 visibility only; accurate
|
||||
-- tokenization is Q1 (target Phase 3).
|
||||
function Context:estimate_tokens()
|
||||
local n = #self.system_prompt
|
||||
for _, t in ipairs(self.turns) do
|
||||
n = n + #t.content
|
||||
end
|
||||
return math.floor(n / 4)
|
||||
end
|
||||
|
||||
function Context:reset()
|
||||
self.turns = {}
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user