diff --git a/context.lua b/context.lua index 82f2ca3..b00e955 100644 --- a/context.lua +++ b/context.lua @@ -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