Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions .codecompanion/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Messages in the chat buffer are lua table objects as seen. They contain the role
_meta = {
cycle = 1,
id = 708950413,
estimated_tokens = 20,
tag = "system_prompt_from_config",
},
opts = {
Expand All @@ -25,15 +26,37 @@ Messages in the chat buffer are lua table objects as seen. They contain the role
}, {
_meta = {
cycle = 1,
estimated_tokens = 20,
id = 533315931,
sent = true
},
opts = {
visible = true
},
role = "user"
content = "Are you working?",
}, {
content = "Are you working? Sharing a file with you",
},
{
_meta = {
cycle = 1,
estimated_tokens = 3556,
id = 1048633318,
index = 5,
sent = true,
source = "editor_context",
tag = "file"
},
content = "<attachment filepath=\"/Users/Oli/some_path/some_file.lua\">An example file</attachment>",
context = {
id = "<file>some_path/some_file.lua</file>",
path = "/Users/Oli/some_path/some_file.lua",
},
opts = {
visible = false
},
role = "user"
},
{
_meta = {
cycle = 1,
id = 1141409506,
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This is a Neovim plugin written in Lua, which allows developers to code with LLM
- **Naming:** snake_case for files/functions, PascalCase for classes, underscore prefix for private functions
- **Explicit names:** `pattern` not `pat`, `should_include` not `include_ok`
- **Readable code:** names, variables, and control flow should read like clean English. Avoid generic names like `ctx` — use domain-specific names (`permission`, `request`, `source`)
- **Plain language:** avoid jargon shortcuts in code, comments, commit messages, and chat. Don't say "no-op" — say what the code actually does ("returns unchanged", "does nothing", "skipped because already edited")
- **Function params:** prefer a single table argument over positional args
- **Error handling:** `pcall` + `log:error()`, return nil on failure
- **Type annotations:** LuaCATS for public APIs. Keep doc blocks concise — one description line, params should be self-explanatory without inline comments
Expand Down
47 changes: 36 additions & 11 deletions lua/codecompanion/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -694,17 +694,24 @@ If you are providing code changes, use the insert_edit_into_file tool (if availa
},
opts = {
context_management = {
trigger = 0.75, -- Compaction starts at 75% of the context window limit
enabled = function(adapter)
if adapter.type ~= "http" then
return false
end
-- Anthropic and OpenAI have their own server-side compaction
if adapter.vendor and (adapter.vendor == "anthropic" or adapter.vendor == "openai") then
return false
end
return true
end,
---@type boolean|fun(adapter: CodeCompanion.HTTPAdapter|CodeCompanion.ACPAdapter): boolean
enabled = true,

editing = {
trigger = 0.65, -- 65% of the context window
exclude_tools = { "memory" }, -- tools whose result are never edited
keep_cycles = 3, -- preserve tool results from the last N cycles
},

compaction = {
trigger = 0.85, -- 85% of the context window

---The adapter to use for compaction. Defaults to the current chat adapter
---@type nil|string|{ name: string, model:string }
adapter = nil,

fallback_to_chat_adapter = false, -- on failure, retry with the chat adapter?
},
},

blank_prompt = "", -- The prompt to use when the user doesn't provide a prompt
Expand Down Expand Up @@ -1354,6 +1361,24 @@ M.setup = function(args)
M.config.interactions.chat.editor_context = nil
end

-- TODO: Deprecate in v20.0.0 and remove in v21.0.0
-- Legacy `context_management.trigger` migrates to `context_management.compaction.trigger`
local context_management = args.interactions
and args.interactions.chat
and args.interactions.chat.opts
and args.interactions.chat.opts.context_management
if context_management and context_management.trigger ~= nil then
vim.notify(
"[CodeCompanion] `context_management.trigger` is deprecated. Use `context_management.compaction.trigger` instead.",
vim.log.levels.WARN,
{ title = "CodeCompanion" }
)
if not (context_management.compaction and context_management.compaction.trigger ~= nil) then
M.config.interactions.chat.opts.context_management.compaction.trigger = context_management.trigger
end
M.config.interactions.chat.opts.context_management.trigger = nil
end

M.config.interactions.chat.keymaps = remove_disabled_keymaps(M.config.interactions.chat.keymaps)
M.config.interactions.cli.keymaps = remove_disabled_keymaps(M.config.interactions.cli.keymaps)
M.config.interactions.inline.keymaps = remove_disabled_keymaps(M.config.interactions.inline.keymaps)
Expand Down
2 changes: 1 addition & 1 deletion lua/codecompanion/interactions/chat/buffer_diffs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ local diff = vim.text.diff or vim.diff

---@class CodeCompanion.BufferDiffs
---@field buffers table<number, CodeCompanion.BufferDiffs.State> Map of buffer numbers to their states
---@field augroup integer The autocmd group ID
---@field augroup number The autocmd group ID
---@field sync fun(self: CodeCompanion.BufferDiffs, bufnr: number): nil Start syncing a buffer
---@field unsync fun(self: CodeCompanion.BufferDiffs, bufnr: number): nil Stop syncing a buffer
---@field get_changes fun(self: CodeCompanion.BufferDiffs, bufnr: number): boolean, table
Expand Down
91 changes: 91 additions & 0 deletions lua/codecompanion/interactions/chat/context_management/editing.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
--=============================================================================
-- Context Editing
--
-- Replaces aged messages in the chat buffer to reduce the token count. This
-- is done by mutating the message object, in-place.
--
-- Currently, only tool results are edited.
--
-- Sources:
-- https://platform.claude.com/docs/en/build-with-claude/context-editing
--=============================================================================

local tokens = require("codecompanion.utils.tokens")

local M = {}

M.PLACEHOLDERS = {
tool_result = "<important>Tool result cleared to save context. Re-run the tool if you need this output</important>",
}

---@class CodeCompanion.ContextManagement.Editing.Opts
---@field current_cycle integer The cycle the chat buffer is currently on
---@field exclude_tools? string[] Tool names whose results are never edited
---@field keep_cycles integer Preserve tool results from the most recent N cycles

---Builds a map of tool call_id to tool name
---@param messages CodeCompanion.Chat.Messages
---@return table<string, string>
local function map_tool_calls(messages)
local map = {}
for _, msg in ipairs(messages) do
if msg.tools and msg.tools.calls then
for _, call in ipairs(msg.tools.calls) do
local fn = call["function"]
if call.id and fn and fn.name then
map[call.id] = fn.name
end
end
end
end
return map
end

---Edit tool result messages older than the keep_cycles window
---@param messages CodeCompanion.Chat.Messages
---@param opts CodeCompanion.ContextManagement.Editing.Opts
---@return number Number of messages cleared
local function tool_results(messages, opts)
local exclude = {}
for _, name in ipairs(opts.exclude_tools or {}) do
exclude[name] = true
end

local tool_names = map_tool_calls(messages)
local cutoff = opts.current_cycle - opts.keep_cycles
local placeholder = M.PLACEHOLDERS.tool_result
local cleared = 0

for _, msg in ipairs(messages) do
local is_tool_result = msg.role == "tool" and msg.tools and msg.tools.call_id
if is_tool_result then
local context_management = msg._meta and msg._meta.context_management
local already_edited = context_management and context_management.edited
local cycle = msg._meta and msg._meta.cycle
local tool_name = tool_names[msg.tools.call_id]
local excluded = tool_name and exclude[tool_name]

if not already_edited and not excluded and cycle and cycle <= cutoff then
msg.content = placeholder
msg._meta.estimated_tokens = tokens.calculate(placeholder)
msg._meta.context_management = msg._meta.context_management or {}
msg._meta.context_management.edited = true
cleared = cleared + 1
end
end
end

return cleared
end

---Replace aged messages
---@param messages CodeCompanion.Chat.Messages
---@param opts CodeCompanion.ContextManagement.Editing.Opts
---@return CodeCompanion.Chat.Messages messages
---@return number Number of messages cleared
function M.apply(messages, opts)
local cleared = tool_results(messages, opts)
return messages, cleared
end

return M
26 changes: 16 additions & 10 deletions lua/codecompanion/interactions/chat/helpers/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -345,28 +345,34 @@ function M.format_viewport_for_llm(buf_lines)
return table.concat(formatted, "\n\n")
end

---Returns the number of tokens that trigger context management
---Returns the number of tokens that trigger context management for a given operation
---@param adapter CodeCompanion.HTTPAdapter
---@param opts? { operation?: "editing"|"compaction" } defaults to "compaction"
---@return number
function M.trigger_context_management(adapter)
if adapter.type ~= "http" then
function M.trigger_context_management(adapter, opts)
opts = opts or {}
local operation = opts.operation or "compaction"

local context_management = config.interactions.chat.opts.context_management
local settings = context_management and context_management[operation]
local trigger = settings and settings.trigger
if trigger == nil then
return 0
end

local ok
local trigger_tokens = config.interactions.chat.opts.context_management.trigger
if trigger_tokens < 1 then
ok, trigger_tokens = pcall(function()
return math.floor(trigger_tokens * adapter.schema.model.choices[adapter.schema.model.default].meta.context_window)
if trigger < 1 then
local ok
ok, trigger = pcall(function()
return math.floor(trigger * adapter.schema.model.choices[adapter.schema.model.default].meta.context_window)
end)

if not ok then
log:error("Could not get evaluate the trigger for context management in the `%s` adapter", adapter.name)
log:error("Could not evaluate the %s trigger for context management in the `%s` adapter", operation, adapter.name)
return 0
end
end

return trigger_tokens
return trigger
end

return M
13 changes: 6 additions & 7 deletions lua/codecompanion/utils/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,13 @@ function M.parse_iso8601(iso)
return nil
end

---@type osdateparam
local date = {
year = tonumber(year) --[[@as integer]],
month = tonumber(month) --[[@as integer]],
day = tonumber(day) --[[@as integer]],
hour = tonumber(hour) --[[@as integer]],
min = tonumber(min) --[[@as integer]],
sec = tonumber(sec) --[[@as integer]],
year = tonumber(year), --[[@as number]]
month = tonumber(month), --[[@as number]]
day = tonumber(day), --[[@as number]]
hour = tonumber(hour), --[[@as number]]
min = tonumber(min), --[[@as number]]
sec = tonumber(sec), --[[@as number]]
}

return os.time(date)
Expand Down
Loading
Loading