-- context.lua — in-memory conversation history + token budget. -- 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 = {} -- 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) 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 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 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 -- 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