# Context rot là vấn đề của harness

[Paul Iusztin](https://homus.dev/speakers/paul-iusztin) · [Decoding AI](https://homus.dev/orgs/decoding-ai)

> Bốn kỹ thuật trong harness giữ context window của coding agent luôn high-signal: memory, skills với progressive disclosure, LSP server và compaction.

Topics: Context Engineering, Coding Agents, Skills

Canonical: https://homus.dev/talks/context-rot-is-a-harness-problem

Video được Opik, Kitaru và một nền tảng cloud tài trợ.

## 1. Mỗi token đều có giá, và lỗi hiếm khi nằm ở model

Paul mở đầu bằng một nhận xét thẳng: mỗi token bạn đưa vào context window đều tốn tiền, đều cộng thêm latency, và tới một lúc nào đó sẽ làm agent của bạn chạy tệ đi. Khi agent bắt đầu mắc lỗi, nguyên nhân hiếm khi là model. Thường thì context window đã bị nhồi đầy file cũ (stale files), tool output, và một lịch sử hội thoại mà model không cần tới. Vì thế, đổi từ model của OpenAI sang model của Anthropic cũng sẽ không sửa được gì.

Từ đó anh đưa ra bốn kỹ thuật mà chính anh dùng để giữ context window ở trạng thái high signal (nhiều tín hiệu, ít nhiễu) mà không cần đụng vào model. Bốn kỹ thuật này trả lời bốn câu hỏi: agent nên nhớ gì giữa các session, vì sao agent không nạp thứ gì cho tới khi thật sự cần, feedback loop rẻ nhất nằm ở đâu, và context window được cắt gọn thế nào trước khi nó "mục" (rot).

![Slide The 4 techniques I use to keep my context high-signal without touching the model với bốn ô](https://homus.dev/photos/dx77BRFZ0_M-0023.jpg)

Bốn kỹ thuật giữ context high-signal mà không đụng vào model: điều agent nên nhớ giữa các session, vì sao agent không nạp gì cho tới khi cần, nơi feedback loop rẻ nhất, và cách cắt gọn context window trước khi nó mục.

## 2. Vòng đời của context trong một session coding agent

Để thấy vì sao cần bốn kỹ thuật đó, Paul bắt đầu với vòng đời của context trong một session của coding agent. Hình dung đây là context window của bạn. Ngay trước khi mọi thứ bắt đầu (vòng lặp số 0), harness đã nạp vào: system prompt, mô tả của các tool, file AGENTS.md, file MEMORY.md (lát nữa sẽ nói nó là gì), và tên cùng mô tả của các skill.

Ở vòng lặp tiếp theo, agent làm việc của nó và có thể quyết định đưa ra một số tool input. Các tool đó được thực thi bên trong harness, nơi output của chúng được thu lại. Agent cũng có thể quyết định nạp một skill cụ thể, nên file SKILL.md đi vào context. Sang vòng lặp sau, nó dùng read tool để nạp một số file và script của skill đó. Ở vòng tiếp nữa, nó gọi write tool, rồi quyết định gọi một LSP server để kiểm tra output của một file nào đó. Và cứ thế tiếp diễn mãi.

![Sơ đồ The context lifecycle of a coding session với các cột Iter 0 tới Iter N](https://homus.dev/photos/dx77BRFZ0_M-0041.jpg)

Context window lớn dần qua từng vòng lặp: Iter 0 có system prompt, mô tả tool, AGENTS.md, MEMORY.md, mô tả skill; Iter 1 thêm tool input, tool output, SKILL.md; Iter 2 thêm read tool, skill files, skill scripts; Iter 3 thêm write tool và output của LSP server.

Bạn sẽ thấy ngay ý chính: context window cứ phình ra, phình ra mãi. Và việc của chúng ta với những kỹ thuật context engineering này là giữ cho context window được kiểm soát, để tỷ lệ tín hiệu trên nhiễu (signal over noise) luôn cao.

## 3. Decode: coding agent được build từ đầu để học

Trong video này và cả loạt bài, Paul dùng code của Decode, một coding agent anh tự build từ đầu, chủ yếu cho mục đích giáo dục. Mọi đoạn code minh hoạ phía sau đều lấy từ harness của Decode.

![Terminal của Decode với danh sách lệnh demo và lệnh /compact](https://homus.dev/photos/dx77BRFZ0_M-0139.jpg)

Decode chạy trong terminal: bật tracing lên Opik, có các mode edit, plan, bypass, và một danh sách skill demo gọi bằng `/demo-`. Khung dưới là lệnh `/compact`, nén khoảng 149.539 token thành một bản tóm tắt cộng 27 message gần nhất.

## 4. Kỹ thuật 1, memory: AGENTS.md và MEMORY.md

Kỹ thuật context engineering đầu tiên, và theo Paul là quan trọng nhất, là memory. Memory chia làm hai dạng.

Dạng thứ nhất là chỉ dẫn viết tay (manual instructions), dựa trên file AGENTS.md. Là người dùng coding agent, chúng ta đã quá quen với nó: trong file AGENTS.md, bạn tự tay ghi ra toàn bộ kiến thức về domain mà coding agent cần để làm việc trong codebase của bạn.

Dạng thứ hai được trích xuất tự động (automatically extracted): file MEMORY.md. Cách này khá giống những gì Claude Code đang làm: từ mỗi cuộc hội thoại, nó dùng một kỹ thuật riêng để trích ra các sở thích của bạn, tức là bạn thích vận hành coding agent đó theo cách nào.

![Slide 1/ Memory: Manual Instructions với AGENTS.md và Automatically Extracted với MEMORY.md](https://homus.dev/photos/dx77BRFZ0_M-0152.jpg)

Hai dạng memory. Bên trái, vòng lặp giữa người dùng, AGENTS.md và agent: con người viết chỉ dẫn. Bên phải, vòng lặp tự động: cuộc hội thoại đổ vào MEMORY.md, được nén lại sau 200 dòng, rồi quay về agent.

File MEMORY.md này được nén để luôn dưới 200 dòng, để tránh nó phình quá to và chiếm hết context window. Còn file AGENTS.md thì luôn được bơm vào agent ở mỗi session.

Về cách hoạt động, phần implement của AGENTS.md khá cơ bản: bạn định nghĩa một file Markdown, lúc nào cũng nạp nó vào context window, thế là xong. Câu hỏi thú vị hơn là MEMORY.md được tạo ra như thế nào.

## 5. MEMORY.md được tạo ra thế nào

Trên slide, bên trái là hàm chính cho thấy cách nó vận hành. Hàm này được gọi mỗi khi người dùng gõ lệnh quit hoặc lệnh clear. Lý do: ngay trước khi vứt bỏ cuộc hội thoại, chúng ta muốn thu lại thông tin từ nó.

![Slide How is MEMROY.md created với hàm extract_on_exit và ví dụ một file MEMORY.md](https://homus.dev/photos/dx77BRFZ0_M-0245.jpg)

Hàm `extract_on_exit` tóm tắt session, nối bản tóm tắt vào file kèm thời điểm, và nén file nếu bật memory compression. Bên phải là bốn đặc điểm: chạy khi quit và khi `/clear`, là append-only log, mỗi cuộc hội thoại một câu kèm ngày, giới hạn 200 dòng hoặc 25.000 byte. Bên dưới là bốn dòng mẫu của một MEMORY.md, mỗi dòng một ngày và một câu.

Đoạn code trên slide, đúng như hiển thị:

```
async def extract_on_exit(messages: list[ModelMessage], cwd: Path) -> None:
summary = await summarize_session(messages, model_or_settings=settings)
append_session_summary(cwd, summary, now=_utc_now())

if settings.memory_compression_enabled:
await compress_memory_file(cwd, model_or_settings=settings)
```

File MEMORY.md có cấu trúc là một log chỉ được ghi nối thêm (append-only log), như ví dụ ở phía dưới slide: với mỗi cuộc hội thoại, ta thu lại đúng một câu kèm ngày nó được ghi. Trong ví dụ có bốn mục trong log, mỗi mục là một ngày và một câu rút ra từ cả cuộc hội thoại đó. Chẳng hạn một dòng ghi rằng hôm đó đã viết một script Python lấy dữ liệu GitHub API và tạo một dashboard HTML tự chứa, nền tối; một dòng khác ghi đã làm một game Snake chạy trong terminal bằng Python.

Vì ép việc nén xuống còn một câu, chúng ta buộc LLM chỉ trích ra thông tin ở mức high-level và không được dài dòng. Như đã nói, file được giữ ở mức 200 dòng hoặc 25.000 byte.

Logic khá đơn giản. Đầu tiên, tạo một bản tóm tắt từ cuộc hội thoại. Rồi nối nó vào log. Khi log chạm ngưỡng, trong trường hợp này là 200 dòng, thì nén nó lại. Paul thừa nhận phần nén này chỉ là một bản tóm tắt của một loạt bản tóm tắt, nên trong thực tế không phải lúc nào nó cũng chạy tốt. Còn bước tóm tắt thì thực ra dùng một LLM chuyên biệt, được prompt riêng để tạo ra những bản tóm tắt rất cô đọng như vậy.

## 6. Kỹ thuật 2, skills: chuẩn Agent Skills và progressive disclosure ba tầng

Kỹ thuật context engineering thứ hai mà bạn phải biết là skills. Ai cũng biết skills rồi: là người vận hành coding agent, chúng ta dùng skills ở khắp nơi. Điều thú vị hơn, theo Paul, là hiểu cơ chế progressive disclosure chạy bên dưới. Hiểu nó giúp bạn vừa tạo được skill tốt hơn, vừa mang được cơ chế này sang bất kỳ coding harness nào khác, hay harness nói chung.

Agent Skills là một chuẩn có dạng như sau: một thư mục skill chứa file SKILL.md, nơi bạn định nghĩa prompt, cùng các phần tuỳ chọn như script (Python, TypeScript hay ngôn ngữ khác), reference và asset.

File SKILL.md trông thế này: có `name`, giống như ID của skill, và `description`, phần mô tả skill. Description là thứ lúc nào cũng được ném vào context window, và trên thực tế nó đóng vai trò trigger: nó cho model biết khi nào nên gọi skill này. Bên ngoài phần front matter là chính prompt của skill.

![Slide 2/ Skills với cấu trúc thư mục my-skill, file SKILL.md và sơ đồ ba tầng progressive disclosure](https://homus.dev/photos/dx77BRFZ0_M-0523.jpg)

Bên trái: chuẩn Agent Skills, thư mục `my-skill/` gồm SKILL.md, scripts, references, assets; SKILL.md có front matter `name` và `description` (trigger) rồi tới prompt của skill. Bên phải: ba tầng. Tier 1 chỉ có mô tả của mọi skill; Load Skill đưa thêm SKILL.md (Tier 2); progressive disclosure đưa thêm scripts, references và assets (Tier 3).

```
---
name:
description:
---

```

Quay lại progressive disclosure: nó chia làm ba tầng. Ở tầng một, tên và mô tả của mọi skill lúc nào cũng nằm trong context window. Tiếp theo, khi model quyết định gọi một skill cụ thể, nó chỉ nạp file SKILL.md của skill đó. Cuối cùng, qua progressive disclosure, nếu nó thấy cần thêm tài nguyên để thực thi skill, nó mới nạp script, reference và asset.

Đó chính là progressive disclosure: nạp dần dần, ngày càng nhiều thông tin hơn vào context window, chỉ khi cần.

## 7. Harness nạp skill catalog vào system prompt thế nào

Giờ tới phần code. Theo Paul, phần đáng xem là bên trong implementation, để hiểu agent thật sự gọi skill ra sao và dùng chúng để làm progressive disclosure thế nào.

Trên slide có định dạng kinh điển của một skill, và pipeline nạp skill. Đầu tiên, ta định nghĩa mọi skill trong một thư mục skills (trong Decode là `.decode/skills/`). Sau đó gom chúng thành một skill catalog, về bản chất là một dictionary nối tên skill (ID của nó) với description (trigger của nó).

![Slide How does an agent calls a skill với pipeline Defined Skills, Assemble Skills Catalog, Optional Cap, Skills Catalog, System Prompt](https://homus.dev/photos/dx77BRFZ0_M-0606.jpg)

Pipeline nạp skill: Defined Skills (`.decode/skills/`) → Assemble Skills Catalog → Optional Cap (khoảng 1% context window) → Skills Catalog (name + description) → System Prompt. Bên dưới là hai đoạn code: skill tool và hàm `format_skill_payload`.

Có thể giới hạn catalog này một cách tuỳ chọn, ví dụ ở khoảng 1% context window, vì có người thích ném quá nhiều skill vào agent tới mức làm nổ tung cả context window. "Yeah, guilty of this", Paul thú nhận mình cũng từng như vậy.

Cuối cùng, tất cả được gói thành một chuỗi lớn và đưa vào system prompt. Nhờ vậy agent lúc nào cũng biết có những skill nào ngoài kia, và có thể dùng tên như một ID để xác định skill.

## 8. Skill tool: agent gọi một skill ra sao

Bạn có thể hỏi: vậy agent thật sự gọi một skill bằng cách nào? Câu trả lời là nó có một tool biết cách nạp một skill cụ thể. Đó là skill tool. Tool này nhận đầu vào là tên skill, tức ID của nó như đã nói. Việc nó làm chỉ là nạp skill từ thư mục skills, cụ thể là nạp file Markdown SKILL.md. Về cơ bản là vậy.

```
async def skill(ctx: RunContext[AgentDeps], name: str) -> str:
home = ctx.deps.harness_home or ctx.deps.cwd
catalog = load_skills(home)
found = catalog.get(name)

return format_skill_payload(found, cwd=home)
```

Phần thú vị hơn là nó định dạng skill payload theo cách riêng, như hàm `format_skill_payload` bên phải slide. Hàm này nhìn quanh mọi file khác nằm trong thư mục skill, rồi đặt danh sách đường dẫn của chúng ngay bên cạnh phần thân của skill. Nên output mà agent thực sự nhìn thấy là nội dung SKILL.md cộng với đường dẫn tới mọi script, file Markdown hay hình ảnh khác mà agent có thể nạp nếu cần. Trên slide, phần đuôi (trailer) ghi rằng đây là các file đi kèm skill, hãy dùng đúng các đường dẫn này, đọc chúng bằng tool `read` và chạy file trong `scripts/` bằng `bash`.

Progressive disclosure trong code: skill tool trả về thân SKILL.md kèm đường dẫn tới mọi file đi kèm; ở vòng lặp sau, agent tự quyết định dùng read tool để nạp file nào.

Rồi ở vòng lặp tiếp theo, vì agent nhìn thấy mọi khả năng có sẵn, nó có thể quyết định, để đạt được mục tiêu (thường được định nghĩa trong SKILL.md), dùng read tool nạp thêm những asset nó cần vào context window. Đó chính là progressive disclosure: một sự kết hợp giữa việc có sẵn mọi đường dẫn trong context window và các read tool. Chỉ vậy thôi. Paul nói thêm: trên thực tế, bạn có thể dùng kỹ thuật này theo rất nhiều cách khác nữa.

## 9. Kỹ thuật 3, LSP server: active và passive

Kỹ thuật context engineering thứ ba là Language Server Protocol server, hay LSP server. Đây là một kỹ thuật được chôn khá sâu bên trong harness, nhưng theo Paul thì rất thú vị.

LSP server thực ra có ở khắp nơi. Chúng nằm trong mọi IDE bạn đang dùng. Ví dụ, khi bạn giữ Control hoặc Command rồi bấm vào một hàm hay một biến và thấy mọi chỗ tham chiếu tới nó trong codebase, đó là một LSP server chạy phía sau. Hay bộ phân tích tĩnh (static analyzer) tô sáng lỗi cú pháp trong file Python hoặc TypeScript của bạn cũng do một LSP server làm. Nên thật ra, mỗi khi bạn cài hỗ trợ cho một ngôn ngữ lập trình mới trong IDE, bạn cũng đang cài một LSP server bên dưới.

Điều ta muốn ở đây là lấy toàn bộ logic đó và trao nó cho agent. Việc này lại có hai dạng.

![Sơ đồ 3/ The Language Server Protocol (LSP) server với hai nhánh Active và Passive](https://homus.dev/photos/dx77BRFZ0_M-0842.jpg)

Hai cách nối agent với LSP server. Active: agent gọi LSP tool để hỏi definitions, references, diagnostics. Passive: write/edit tool tự lấy diagnostics, và LSP server trả message cùng diagnostics thẳng về agent.

- **Active:** ta đưa cho agent một LSP tool, để nó chủ động hỏi LSP server về definition của biến hay hàm, về reference, và về diagnostics lỗi cú pháp.

- **Passive:** ta biết rằng sau mỗi lần write hoặc edit, ta muốn kiểm tra xem cú pháp của file đó đã đúng chưa. Nên ta quy định: sau mỗi lần gọi một trong hai tool này, ta gọi LSP server để lấy diagnostics cú pháp, xem file đã được viết đúng chưa, rồi tự động đưa kết quả về cho agent làm feedback.

## 10. LSP tool, chạy bằng ty server bên trong harness

Bên trong coding agent, cách thứ nhất là LSP tool, nơi agent có thể chủ động tra cứu definition và reference. Agent truyền vào một operation: `definition`, `references` hoặc `hover`. Nếu không truyền gì, tool sẽ chạy diagnostics trên file đó.

```
async def lsp(
ctx: RunContext[AgentDeps],
op: str,
path: str,
line: int | None = None,
column: int | None = None,
) -> str:
if op == "definition":
return await _run_definition(ctx, path, line, column)
if op == "references":
return await _run_references(ctx, path, line, column)
if op == "hover":
return await _run_hover(ctx, path, line, column)

return await _run_diagnostics(ctx, path)
```

Bên dưới, điều thật sự diễn ra là ta khởi một process bên trong harness. Trong trường hợp này là một ty server: một LSP server do chính đội làm ra package manager uv phát triển, và tất nhiên được viết bằng Rust. Ta chạy LSP server này bên trong harness, rồi bọc nó bằng một client biết cách nói chuyện với server. Qua client này, agent có thể giao tiếp với LSP server và hỏi definition, reference, v.v.

```
from decode.services.lsp.client import LspClient

process = await asyncio.create_subprocess_exec(
"ty", "server",
stdin=PIPE, stdout=PIPE, cwd=root,
)
client = LspClient(process, root)
await client.initialize()
```

Về cơ bản, LSP server làm hết phần việc nặng, còn ta chỉ dựng một cây cầu giữa hai bên.

Cách Decode nối agent với LSP: LSP tool chuyển yêu cầu qua một client tới ty server, một process mà harness tự khởi chạy.

## 11. Một lần gọi LSP tool trong trace của Opik

Paul muốn cho xem cách nó chạy trong một session thật. Anh mở Opik, công cụ observability anh dùng để thu mọi trace. Ở đây có một trace mà agent đã dùng LSP tool.

![Giao diện Opik hiển thị trace chat_turn và output của model với một tool_call tên lsp](https://homus.dev/photos/dx77BRFZ0_M-1120.jpg)

Một trace `chat_turn` trong Opik. Bên phải là output của model: phần thinking ghi rằng người dùng muốn biết tool "bash" xuất hiện bao nhiêu lần và đã yêu cầu dùng LSP tool; sau đó là một `tool_call` tên `lsp` với arguments op là "references", path là `src/decode/tools/bash.py`, line 35, column 1.

Ở đây ta thấy tên tool, thấy đây là một lần gọi tool, và thấy phần arguments, phần thú vị hơn cả. Operation là tìm reference cho bash tool được định nghĩa trong file này. Sau khi tool chạy bên trong harness, tức là sau khi gọi LSP server, ta thấy mọi chỗ bash tool được dùng trong codebase.

Output cho biết nó được dùng bao nhiêu lần trong codebase, rồi liệt kê mọi file, dòng và cột nơi hàm đó được dùng. Nó được dùng ở chỗ nó được định nghĩa trong file bash; rồi trong file flow.py, ba lần; và một loạt lần trong các file test. Output này được đưa ngược về agent để nó làm việc tiếp và tiến thêm một bước về phía mục tiêu. Agent có thể dùng tool này theo cách tương tự để tìm reference, definition và cả diagnostics.

## 12. Passive diagnostics sau mỗi lần write và edit

Tiếp theo là implementation của cách thứ hai: passive diagnostics trên các lần gọi write và edit. Cách này đơn giản hơn nhiều. Sau mỗi lần tool write hoặc edit được gọi, ta gọi hàm `_enrich`. Hàm này về cơ bản chỉ gọi tính năng diagnostics của LSP server trên file vừa được sửa hoặc vừa được viết mới.

```
def _enrich(base: str, cwd: Path, path: str) -> str:
summary = _format_lsp_errors(lsp_service.diagnostics_on_edit(cwd, path))
if summary is None:
return base
return f"{base}\n\n{summary}"

def _format_lsp_errors(diagnostics: list[Diagnostic] | None) -> str | None:
...
errors = [d for d in diagnostics if d.severity == _LSP_ERROR_SEVERITY]
shown = errors[:_LSP_DIAGNOSTICS_LIMIT]
lines = [f"LSP diagnostics ({settings.lsp_server_command}) - fix these:"]
...
return "\n".join(lines)
```

Ở đây ta chỉ lấy diagnostics của LSP server và định dạng chúng thành một chuỗi lớn, gọi là summary output. Nếu summary là None, nghĩa là file được viết đúng, không có lỗi cú pháp nào; hoặc với một ngôn ngữ có thể biên dịch, nó còn đi xa hơn và kiểm tra xem code có biên dịch được không. Còn nếu có lỗi, ta nối summary vào base message, về cơ bản là trạng thái hiện tại của context window tính tới thời điểm đó.

Passive diagnostics: mỗi lần write hoặc edit, harness tự gọi LSP server và chỉ thêm vào context khi có lỗi.

Như vậy, sau mỗi lần dùng write hoặc edit tool, bạn có một feedback loop nhanh, được đưa thẳng vào agent loop. Thay vì agent phải thử chạy file đó, hoặc chạy cả chương trình chứa file đó, nó nhận ngay feedback và biết có gì sai với đoạn code vừa viết. Nó không cần phải đoán xem có lỗi cú pháp nào không.

## 13. Kỹ thuật 4: clear, compaction và micro compaction

Kỹ thuật context engineering thứ tư trong video là compaction, "the good old compaction", và bản thân nó lại chia làm ba lựa chọn. Mục tiêu của compaction là lấy một context window lớn đã bị lấp đầy, ví dụ đã đầy tới 80%, và cố giữ lại càng nhiều thông tin càng tốt trong khi thu nhỏ context window.

Vì sao phải làm vậy? Vấn đề thứ nhất: hiệu năng của coding agent bắt đầu "mục" khi context window lớn lên, vì càng nhiều nhiễu trong context window thì agent càng bị rối. Vấn đề thứ hai: bản thân context window có giới hạn, nên bạn không thể nhét thông tin vào vô hạn. Ý tưởng là lấy context window khổng lồ đó và thu nhỏ nó hết mức có thể.

![Slide 4/ Compaction với ba cột /clear, /compact (at 80%), /microcompact (at 60%)](https://homus.dev/photos/dx77BRFZ0_M-1353.jpg)

Ba cách thu gọn context. `/clear` chỉ giữ system prompt. `/compact` (ở 80%) giữ system prompt, một Summary và phần Tail. `/microcompact` (ở 60%) giữ nguyên tool input và message, nhưng thay tool output bằng placeholder.

**Lựa chọn một là clear.** Nó đơn giản vứt hết mọi thứ, và bạn bắt đầu lại với một trang trắng chỉ có system prompt. Cách này ổn, nhưng không hay: bạn mất toàn bộ context và phải làm lại từ đầu.

**Lựa chọn hai là compaction.** Vẫn có system prompt; rồi nó lấy trạng thái hiện tại của các message và tạo một bản tóm tắt, chẳng có gì ghê gớm. Điều thú vị hơn là bạn còn đặt phần tail lên trên bản tóm tắt đó, tức là những message cuối cùng của cuộc hội thoại. Như vậy, ít nhất bạn có bản tóm tắt làm cái nhìn tổng quan về những gì đang diễn ra, và có thêm vài bước cuối agent vừa đi qua, để nó không mất hết context chi tiết, vì những message cuối thường là quan trọng nhất. Compaction thường được kích hoạt ở mức 80% context window. Ngưỡng này có thể thay đổi, nhưng theo Paul đó là điều Claude Code đang làm ở thời điểm video được đăng (25/09/2026).

**Lựa chọn ba là micro compaction**, kích hoạt ở mức 60% context. Paul gọi đây là một mẹo rất thú vị. Nó nhìn vào mọi tool output, và thay vì tóm tắt gì cả, nó chỉ thay các tool output bằng một placeholder. Cách này hiệu quả vì tool output thường chiếm phần lớn context window: có thể là output sau khi bạn ghi một file, output sau khi gọi LSP server, hay đủ thứ khác. Đây thường là nơi agent tương tác với môi trường của nó, và có thể lấp đầy context window rất nhanh.

Bạn có thể kết hợp hai kỹ thuật này, và về cơ bản là không áp placeholder lên phần tail của tool output. Theo Paul, cách này chạy rất tốt.

## 14. Implement compaction

Giờ tới implementation của hàm compact. Đầu tiên là trigger, nơi agent quyết định khi nào gọi logic compact. Tham số quan trọng nhất là `reserve`: một khoảng đệm tính theo phần trăm, định nghĩa phần context window ta vẫn muốn để trống cho tới khi compaction được kích hoạt. Trong trường hợp này là 20%, tương đương việc gọi compaction khi context window đã đầy 80%.

![Slide How does /compact work với hàm should_compact và class AgentTurnHandler](https://homus.dev/photos/dx77BRFZ0_M-1627.jpg)

Trên: hàm `should_compact`, trả True khi số input token vượt ngưỡng tính từ kích thước window và `reserve`, tức context window ở mức 80%. Dưới: hàm `compact` trong `AgentTurnHandler` cắt tail, tóm tắt lịch sử, rồi thay lịch sử bằng summary cộng tail.

Phần thú vị nhất nằm trong agent loop, nơi có logic compact:

```
class AgentTurnHandler:
...

async def compact(self) -> CompactOutcome:
split = split_tail(
self.message_history, keep_recent_tokens=settings.compaction_keep_recent_tokens
)
skeleton = await summarize_for_compaction(
self.message_history, model=self._compaction_model
)

before_tokens = self._last_input_tokens
summary_message = build_summary_message(skeleton)
tail = self.message_history[split:]

self.message_history = [summary_message, *tail]

return CompactOutcome.COMPACTED
```

Đầu tiên ta quyết định điểm cắt: muốn giữ lại phần tail nào sau khi compaction. Tiếp theo, ta tóm tắt lịch sử message, rồi định dạng bản tóm tắt cho đúng. Ta lấy tail, đơn giản là lấy những message cuối trong lịch sử, thế thôi. Rồi ta thay lịch sử message bằng message tóm tắt cộng tail, và tiếp tục chạy. Ở đây ta không truyền system prompt vào, vì agent loop tự động nạp nó.

Ví dụ, trong harness Decode mà Paul đã implement, bạn có thể gọi nó bằng lệnh `/compact`, hoặc nó sẽ tự được gọi khi mức dùng context vượt 80%, giống như Claude Code hay các harness phổ biến khác. Trong terminal của Decode trên slide, lệnh `/compact` báo đã nén khoảng 149.539 token thành một bản tóm tắt cộng 27 message gần nhất.

## 15. Implement micro compaction

Cuối cùng là implementation của hàm micro compaction. Thuật toán này cũng được cắm vào agent loop, và chạy như sau.

![Slide How does /microcompaction work với hàm microcompact và ghi chú keep latest messages, keep only tools and retries, replace](https://homus.dev/photos/dx77BRFZ0_M-1749.jpg)

Hàm `microcompact` với ba ghi chú bên phải: giữ nguyên các message mới nhất (tail), chỉ đụng tới tool output và retry prompt, và thay nội dung của chúng bằng placeholder "[tool output elided by microcompaction]".

```
_MICRO_PLACEHOLDER = "[tool output elided by microcompaction]"

def microcompact(
messages: list[ModelMessage],
*,
keep_recent_tokens: int,
placeholder: str = _MICRO_PLACEHOLDER,
) -> list[ModelMessage]:
boundary = split_tail(messages, keep_recent_tokens=keep_recent_tokens)

new_messages: list[ModelMessage] = []
for index, message in enumerate(messages):
if index >= boundary or not isinstance(message, ModelRequest):
new_messages.append(message)
continue

new_parts = list(message.parts)
changed = False
for position, part in enumerate(message.parts):
if not isinstance(part, ToolReturnPart | RetryPromptPart):
continue
new_parts[position] = dataclasses.replace(part, content=placeholder)

new_messages.append(dataclasses.replace(message, parts=new_parts))

return new_messages
```

Ta duyệt qua mọi message và xem chúng có thuộc phần tail hay không. Nếu thuộc tail, ta không làm gì cả, giữ nguyên tool output đầy đủ. Nếu không thuộc tail, ta duyệt tiếp từng phần bên trong message, xem đó có phải tool output hay retry prompt không. Nếu phải, ta chỉ lấy phần đó của message và thay bằng một placeholder. Thế là xong.

Sau đó ta trả về lịch sử message mới và thay nó vào session. Ở vòng lặp tiếp theo của agent loop, chính context window đã gọn này sẽ được đưa cho LLM thay vì toàn bộ lịch sử message.

## 16. Khoá học mã nguồn mở Building a Coding Agent From Scratch

Để khép lại, Paul nhấn mạnh rằng video này là một phần của một khoá học mã nguồn mở lớn hơn tên là Building a Coding Agent From Scratch, gồm tổng cộng tám bài. Trong đó, bạn sẽ học cách build Decode, một coding agent tương tự Claude Code: từ cách thiết kế nó, cách build các thành phần harness engineering và context engineering, cho tới cách deploy nó lên cloud và cách chạy eval trên nó.

Khoá học nằm trên GitHub; trên màn hình có mã QR và URL của repo, và anh cũng để link trong phần mô tả video. Khoá học hoàn toàn miễn phí, nên bạn cứ thoải mái clone, fork, làm gì tuỳ ý và tận hưởng.

## Nguồn và liên kết

- Tên gốc: Context Rot Is a Harness Problem
- Sự kiện: [YouTube](https://homus.dev/events/youtube)
- Video gốc: [https://www.youtube.com/watch?v=dx77BRFZ0_M](https://www.youtube.com/watch?v=dx77BRFZ0_M)

- Kênh YouTube Decoding AI (Paul Iusztin): [youtube.com/channel/UCkQvWpuLUv_pTMfQXAE-C6Q](https://www.youtube.com/channel/UCkQvWpuLUv_pTMfQXAE-C6Q)

- Khoá học mã nguồn mở Building a Coding Agent From Scratch, chứa code của Decode: [github.com/decodingai-magazine/building-a-coding-agent-from-scratch-course](https://github.com/decodingai-magazine/building-a-coding-agent-from-scratch-course)

- Bài viết đi kèm của Paul Iusztin trên Decoding AI, "Context Engineering for Coding Agents": [decodingai.com/p/context-engineering-for-coding-agents](https://www.decodingai.com/p/context-engineering-for-coding-agents)

- ty, Python type checker và language server viết bằng Rust của Astral (đội làm uv): [github.com/astral-sh/ty](https://github.com/astral-sh/ty)

Nguồn: https://homus.dev/talks/context-rot-is-a-harness-problem · cập nhật 2026-10-18
