docs/adr/0006-conversation-compaction.mdXem bản gốc Bản dịch đầy đủ theo đúng cấu trúc tài liệu; code, liên kết và định danh kỹ thuật được giữ nguyên.
0006. Nén hội thoại - nén bộ nhớ trong ngữ cảnh + khi thoát hai tầng tương đối với cửa sổ, trên JSONL
Trạng thái: Được chấp nhận — §3 (nguồn token), §3a/§5 (điểm cắt), §7 (/phản hồi nhỏ gọn) và hậu quả về kích thước đuôi được sửa đổi bởi ADR-0018 Ngày: 2026-06-26
Bối cảnh
ADR-0002 §8/§9 đã xây dựng lớp ngữ cảnh M1 — lớp
JSONL chỉ nối thêm nhật ký phiên (context/session_log.py) cấp nguồn decode --resume, và
ghi lại bộ nhớ khi thoát một câu (memory/extract.py::summarize_session) - và **cố ý
nén hoãn lại **, đặt tên summarize_session như "đường may nén chặt M4 phát triển từ." Một phiên dài
message_history phát triển theo hướng cửa sổ ngữ cảnh của mô hình (rủi ro về chi phí/độ trễ/cắt ngắn); riêng biệt,
duy trì mô hình MEMORY.md được giữ dưới giới hạn của nó bằng cách bỏ các dòng cũ nhất (mất các dữ kiện cũ).
ADR này ghi lại quá trình nén dưới dạng ba cơ chế hợp tác cộng với khả năng chi trả cho giao diện người dùng, dưới dạng một tính năng
(nhiệm vụ 041-049, tính năng context-compaction):
- Microcompaction — tầng trong bộ nhớ, giá rẻ, không có LLM giúp loại bỏ các phần thân đầu ra công cụ cũ.
- Nén nén hoàn toàn — Cấp LLM tóm tắt lịch sử cũ hơn; kiên trì vào nhật ký JSONL.
- Nén bộ nhớ — máy nén LLM khi thoát cho
MEMORY.md, thay thế lossy thả cũ nhất. - Context Gauge — vòng tròn điền vào chân trang đọc cùng một cài đặt cửa sổ.
điểm tiếp xúc pydantic-ai đã được xác minh dựa trên pydantic-ai 2.0.0 đã cài đặt** (§3), vì vậy chúng không lại bị kiện tụng ở hạ lưu.
Quyết định
-
Lưu trữ để nén hoàn toàn: a đã đánh máy
compactiondòng trên nhật ký JSONL chỉ nối thêm hiện có - KHÔNG SQLite. Sự khác biệt được cố ý, ghi lại từ cách diễn đạt "nhật ký hội thoại (SQLite)" của AGENTS.md và hàng "Datastore: SQLite" của Tech Stack. Đối với một agent một tiến trình, một agent, một agent chỉ bổ sung Nhật ký JSONL với dòng điểm kiểm tra độc lập là đủ và sử dụng lại đường nối ADR-0002 §9. SQLite ở lại tùy chọn hoãn lại; nhiệm vụ 048 sửa AGENTS.mdcontext/diễn đạt để(JSONL). Acompactiondòng mang thông điệp tóm tắt được tuần tự hóa và phần đuôi được giữ lại; khi phát lạiload()loại bỏ lịch sử tích lũy tại dòng đó và khởi động lại từ[summary, *tail], sau đó tiếp tục - trước đó các bản sao nguyên văn của phần đuôi được thay thế, không bao giờ được tính hai lần. -
Hai trình kích hoạt trong ngữ cảnh, CẢ HAI đều tự động, cộng với một trình kích hoạt thủ công — tầng đầu tiên rẻ nhất. Sau một lượt vẫn tồn tại ở
WOULD_STOP, và chỉ khicompaction_enabled:if input_tokens >= full_level → đầy đủ; elif input_tokens >= micro_level → microcompaction; khác không có gì. Manual/compact(chỉ ở chế độ nhàn rỗi) buộc phải nén đầy đủ, bỏ qua các ngưỡng vàcompaction_enabled. Microcompaction giảm token cho ** lượt tiếp theo **. -
Đo lường: dự trữ tương đối theo cửa sổ (thay thế lựa chọn ngưỡng phẳng trước đó, theo ý muốn của người dùng yêu cầu). Một cấp độ sẽ kích hoạt khi
input_tokens >= context_window * (1 - reserve), với ** có thể định cấu hình cửa sổ** (compaction_context_window_tokens, mặc định = Cửa sổ nhập liệu của Gemini 2.5 Flash1_048_576— "đặt cái này vào cửa sổ nhập của mô hình đang hoạt động của bạn") và phân số dự trữ trên mỗi tầng:compaction_reserve_fraction = 0.20(bắn hoàn toàn ở mức đầy 80%) vàmicrocompaction_reserve_frag = 0,40(micro fires earlier, at 60% full). INVARIANT:microcompaction_reserve_section > compaction_reserve_section(dự trữ vi mô nhiều hơn → kích hoạt trước), được xác nhận theo mặc định. Cái này cố tình thay thế quan điểm ngưỡng phẳng/"không có cửa sổ cho mỗi mô hình" trước đó của ADR — nhưng cửa sổ là ** một số có thể định cấu hình duy nhất, vẫn không phải là bảng cho mỗi mô hình . ** Đã xác minh pydantic-ai 2.0.0 API: việc sử dụng là một thuộc tính —run.result.usage.input_tokens(run.result.usage()tăng lênTypeError; 1.xrequest_tokenstên đã biến mất);TestModelcư trúinput_tokens(56 cho một lời nhắc ngắn gọn). pydantic-ai'sModelProfilehiển thị trường cửa sổ ngữ cảnh no (đã được xác minh), do đó cài đặt có thể định cấu hình là hợp đồng duy nhất - không tự động phát hiện. Dự phòng an toàn:input_tokens == 0(không có người ở) → không có đám cháy cấp nào lượt đó.3a. Tính năng nén vi mô CHỈ có trong bộ nhớ — không bao giờ tồn tại. Nó xây dựng lại các tin nhắn cũ hơn giữ lại cái đuôi gần đây, thay thế từng cái
ToolReturnPart/RetryPromptPartnội dung có phần giữ chỗ ("[tool output elided by microcompaction]") thông quadataclasses.replace(không có đột biến tại chỗ). Nó chỉ trống nội dung - không bao giờ xóa một tin nhắn/phần - vì vậy nó không bao giờ có thể mồ côi cặp lệnh gọi công cụ/kết quả (không cần có ranh giới). Nó ** bình thường **. Nó không ghi vào nhật ký JSONL và không di chuyển_persisted_count: nhật ký giữ độ trung thực đầy đủ để phục hồi; tiếp tục phát lại toàn bộ lịch sử và lại microcompact. Đây là sự khác biệt rõ ràng so với việc nén hoàn toàn (mà IS vẫn tồn tại). -
Hình dạng tóm tắt thu gọn đầy đủ: khung Markdown cố định. MỘT công cụ tóm tắt MỚI, đầy đủ hơn (
context/compaction.py), khác biệt vớisummarize_session(dành cho việc ghi lại bộ nhớ) nhưng tái sử dụng nó_resolve_modelkiểu mẫu và phong cách phiên âm. Skeleton (hội tụ bằng opencode và pi):# Conversation summary→## Goal→## Constraints & Preferences→## Progress(Xong · Đang Tiến trình · Bị chặn) →## Key Decisions→## Next Steps→## Critical Context. Bộ xương đầy trở thành một cái đầu tổng hợpModelRequest/UserPromptPart, được đóng khung như một bản tóm tắt của phần trước (rút gọn) cuộc trò chuyện. -
Những gì còn sót lại sau quá trình nén hoàn toàn: thông báo tóm tắt + phần đuôi nguyên văn gần đây, được gắn vào một lần nén Ranh giới. Lịch sử sau khi nén là
[summary_message, *tail]; cái đuôi phù hợpcompaction_keep_recent_tokens(mặc định20_000), cắt quay trở lại ranh giới theo lượt của người dùng để nó không bao giờ tách cặp công cụ-gọi/kết quả. Không có tập tin hydrat hóa lại (được người dùng xác nhận lại: giữ gần đây quay nguyên văn, không đọc lại file). Bản tóm tắt trước, với tư cách là phần tử 0, tạo nên đầy đủ liên tiếp việc nén hợp nhất miễn phí — không có logic hợp nhất. -
Tính nhất quán bất biến (nén hoàn toàn). Nén hoàn toàn sẽ giữ nhật ký JSONL (mới
compactiondòng), trong bộ nhớmessage_history([summary, *tail]) và_persisted_count(đặt lại về điều đó length) theo bước khóa. Ô tô chạy bên trong đoạn bay đơn của ngã rẽ;/compactchỉ chạy khi rảnh rỗi. -
Điều khiển bằng tay.
/compactlà lệnh gạch chéo dành riêng được phân tích cú pháp trước nhánh kỹ năng, chỉ ở chế độ nhàn rỗi (bận → đường thân thiện), buộc phải nén hoàn toàn vớiContextCompactedxác nhận. -
Cấp độ thứ hai — Nén bộ nhớ khi thoát cho
MEMORY.md, ở giới hạn 200 dòng. Khi tệp đạt tớimemory_max_lines(200),compress_memory_file(cwd, *, model_or_settings)tạo nên một cuộc gọi LLM giá rẻ (sử dụng lại_resolve_model) để loại bỏ/hợp nhất các dữ kiện có tín hiệu cao nhất và viết lại theo mũ, được kẹp cứng bằngclip_lines_to_budget. Nối vàoextract_on_exitsau viên đạn ghi ngày tháng nối thêm, kiểm soát bởimemory_compression_enabled(mặc địnhTrue), hoàn toàn không gây tử vong. ** Thả cũ nhất vẫn là dự phòng được đảm bảo** (mức trần luôn ở bên trongappend_session_summaryVÀ dự phòng khi cuộc gọi thất bại/trống) — vì vậy giới hạn LUÔN được thực thi, ngay cả với mô hình không/không thành công. -
Thước đo ngữ cảnh (Khả năng chi trả giao diện người dùng, cùng một cửa sổ = nguồn thông tin chính xác duy nhất). Vòng tròn điền vào chân trang (
tui/render.py::context_gauge) biểu hiệnlast_input_tokens / compaction_context_window_tokensnhư một glyph điền vào bánh○◔◑◕●+ tỷ lệ phần trăm, được tô màu xanh lục/vàng/đỏ theo các dòng điền cấp giống (1 - micro_reserve= 60%,1 - full_reserve= 80%), do đó người dùng xem cách tiếp cận ngữ cảnh sự nén chặt. Nó đọc sạch sẽAgentTurnHandler.last_input_tokenstài sản (không bao giờ là một attr riêng). Lưu ý đầu cuối: một vòng có khoảng cách thực sự không thể hiển thị được bằng một ký tự;○◔◑◕●là thiết bị di động xấp xỉ pie-fill.
Sơ đồ
flowchart TB
subgraph turn["Per-turn cascade (WOULD_STOP) — window-relative"]
measure{"input_tokens vs window*(1-reserve)"}
micro["Microcompaction (NO LLM, in-memory only)<br/>fires >= window*(1-micro_reserve) (~60% full)<br/>blank old ToolReturn/RetryPrompt bodies · idempotent<br/>NOT persisted · log keeps full fidelity"]
full["Full Compaction (LLM, persisted)<br/>fires >= window*(1-full_reserve) (~80% full)<br/>fixed skeleton summary + verbatim tail @ Compaction Boundary"]
line["append_compaction(summary, tail)<br/>{type: compaction} on JSONL — NO SQLite"]
reset["reset history=[summary,*tail]<br/>_persisted_count=len · emit ContextCompacted"]
end
manual(["User /compact (idle)"]) --> full
measure -->|>= full level| full
measure -->|>= micro level| micro
measure -->|below / disabled / 0 tokens| noop["no-op"]
micro --> emitm["emit ContextMicrocompacted"]
full --> line --> reset
line -.persisted.-> replay["load(): compaction line →<br/>discard, restart [summary,*tail], continue"]
replay --> resume(["decode --resume → compacted history,<br/>re-microcompact as needed (full-fidelity log)"])
gauge["TUI footer Context Gauge ○◔◑◕●<br/>last_input_tokens / window · green<60% · yellow 60-80% · red>=80%"]
measure -.same window.-> gauge
subgraph exit["On quit — second level"]
append["append_session_summary()<br/>(drop-oldest clamp = always-on ceiling/fallback)"]
memcap{"MEMORY.md >= 200 lines (memory_max_lines)?"}
memcomp["compress_memory_file() — ONE LLM call<br/>dedupe/merge high-signal · rewrite under caps"]
memfb["fail/blank → drop-oldest fallback (cap enforced)"]
end
append --> memcap
memcap -->|yes & enabled| memcomp
memcap -->|no| memskip["leave file"]
memcomp -->|ok| memdone["write back + hard clamp"]
memcomp -->|fail/blank| memfb
classDef micro fill:#1565c0,stroke:#0d47a1,color:#ffffff;
classDef full fill:#6a1b9a,stroke:#4a148c,color:#ffffff;
classDef store fill:#2e7d32,stroke:#1b5e20,color:#ffffff;
classDef mem fill:#e65100,stroke:#bf360c,color:#ffffff;
classDef ui fill:#00838f,stroke:#005662,color:#ffffff;
classDef plain fill:#eceff1,stroke:#607d8b,color:#102027;
class micro,emitm micro;
class full,reset full;
class line,replay store;
class append,memcap,memcomp,memfb,memdone,memskip mem;
class gauge ui;
class measure,manual,noop,resume plain;
hậu quả
- Đường may nén M1 được thực hiện từ
summarize_sessionmà không làm phiền nó; nén hoàn toàn và việc tái sử dụng nén bộ nhớ giống nhau_resolve_modelmẫu, vì vậy CI vẫn ngoại tuyến. - Ngưỡng tương đối của cửa sổ thay thế các số phẳng trước đó theo yêu cầu của người dùng. Trình kích hoạt là
bây giờ "cửa sổ đầy đến mức nào" (
input_tokens >= window*(1-reserve)), thích ứng với bất kỳ mô hình/ cửa sổ mà người dùng định cấu hình — trong khi vẫn giữ lại một số có thể định cấu hình duy nhất, không phải bảng cho mỗi mô hình. Vì pydantic-ai không hiển thị cửa sổ mô hình nên cài đặt là hợp đồng (không tự động phát hiện sự trôi dạt). - Dự trữ theo từng tầng mang lại khả năng phòng thủ đầu tiên với chi phí thấp nhất: vi nén (không có LLM, trong bộ nhớ) hấp thụ áp lực
đầy từ ~60%; nén hoàn toàn (một cuộc gọi LLM, vẫn tồn tại) chỉ ở mức ~ 80% đầy đủ hoặc trên
/compact. - Tính năng nén vi mô chỉ có trong bộ nhớ — nhật ký JSONL vẫn giữ được độ trung thực hoàn toàn; tiếp tục áp dụng lại nó. Nó không bao giờ có thể mồ côi một cặp công cụ (để trống nội dung, không xóa gì). Đánh đổi: chi tiết đầu ra công cụ cũ rời đi cửa sổ trong ngữ cảnh (có thể phục hồi từ nhật ký) - tiết kiệm chi phí dự kiến.
- Khả năng phục hồi được bảo toàn trên JSONL để nén hoàn toàn; một điểm kiểm tra không đúng định dạng sẽ xuống cấp một cách an toàn
lịch sử chưa được nén lại (khoan dung, giống như
messages-phát lại dòng). - Sự phân kỳ JSONL-over-SQLite là có chủ ý và được ghi lại (§1); nhiệm vụ 048 sửa AGENTS.md.
- Kích thước đuôi sử dụng kích thước thô
chars≈/4ước tính (chỉ giữ lại ranh giới "cũ" vi mô - không bao giờ trình kích hoạt, có thẩm quyền của nhà cung cấp), vì pydantic-ai chỉ hiển thị mức sử dụng tổng hợp trên mỗi chặng. Bị giới hạn (chỉ ảnh hưởng đến mức độ lưu giữ bối cảnh gần đây). Đã xem xét và chấp nhận. - Nén bộ nhớ thay thế loại bỏ lỗi cũ nhất bị mất ở giới hạn 200 dòng bằng nén LLM tín hiệu cao, cũ nhất còn lại là mức dự phòng/trần được đảm bảo - sự thật bền vững tồn tại lâu hơn nhiều so với sự thật thuần túy cho phép cắt ngắn và giới hạn luôn được thực thi ngay cả với mô hình không có/không thành công.
- Bộ đo bối cảnh liên kết cùng một cửa sổ với một cảnh báo hiển thị để người dùng thấy quá trình nén sắp diễn ra; nó các bậc màu được lấy từ cùng một phân số dự trữ (nguồn sự thật duy nhất). Phong cách Glyph là một xấp xỉ điền vào bánh di động, có thể hoán đổi trong một trợ giúp.
- Các lần nén đầy đủ liên tiếp không cần mã hợp nhất — phần tóm tắt trước đó sẽ giữ vai trò chính.
- Không phải mục tiêu (có chủ ý): tập tin tái hydrat hóa (được xác nhận lại), bảng cửa sổ ngữ cảnh cho mỗi mô hình / tự động phát hiện (cửa sổ là một số có thể định cấu hình) và bất kỳ cửa hàng SQLite/bền nào - tất cả đều bị trì hoãn. (Vi tính không còn là mục tiêu nữa; nó là một cấp độ trong phạm vi.)