Harness Academy
Tài liệu gốc 29 7 phútTiếng Việt + English
docs/adr/0018-compaction-cut-points-token-source-and-provider-seam-summarizer.mdXem bản gốc
Ngôn ngữ bài đọc
Bản dịch tiếng Việt

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.

0018. Sửa lỗi nén — Điểm cắt ModelResponse, nguồn token phản hồi cuối cùng, hiển thị kết quả, tóm tắt đường nối của nhà cung cấp

Trạng thái: Được chấp nhận — sửa đổi ADR-0006 §3, §3a, §5, §7 và của nó hậu quả "cỡ đuôi"; ADR-0006 được giữ lại (kiểu sửa đổi một phần theo ADR-0012/0016). Ngày: 2026-07-22

Bối cảnh

Tầng nén của ADR-0006 không bao giờ kích hoạt khi sử dụng agent thực sự và /compact đọc như là một no-op. Đã được xác minh dựa trên nhật ký phiên thực (.decode/sessions/20260722T181859Z_8f85f3c9….jsonl: MỘT lượt = 1 lời nhắc của người dùng + 63 tin nhắn công cụ, không compaction dòng bao giờ hết), hai nguyên nhân gốc rễ độc lập:

  1. Cắt điểm. split_tail (context/compaction.py) điều chỉnh mức cắt giảm ngân sách về mức gần nhất Ranh giới USER-TURN (ADR-0006 §5). Một lượt agent dài có chính xác một — chỉ số 0 — vì vậy snap sụp đổ về 0 = "mọi thứ đều phù hợp". compact() trả về giả âm thầm VÀ microcompact() (chia sẻ split_tail, §3a) chẳng có gì che giấu cả. Cả hai tầng đều không hoạt động chính xác khi bối cảnh một vòng quay nặng nặng về dụng cụ - tồn tại trường hợp nén chặt.
  2. Nguồn token. agent/loop.py cửa hàng run.usage().input_tokens. Theo cài đặt pydantic-ai 1.95.1 (hạ cấp ADR-0009 - ghi chú "thuộc tính 2.0.0" của ADR-0006 §3 không còn nữa mô tả API đã cài đặt), RunUsage được TÍCH LŨY trên mọi yêu cầu trong nhánh (đã xác minh: usage.py:243 tích lũy +=; một yêu cầu cho mỗi vòng công cụ), vì vậy thước đo và cả hai kích hoạt đếm quá mức ~N× cho N vòng công cụ. Kích thước ngữ cảnh thực sự là của phản hồi CUỐI CÙNG mức sử dụng theo yêu cầu - ModelResponse.usage: RequestUsage (đã xác minh messages.py:2062; nhà máy mặc định, nên không được phổ biến ⇒ input_tokens == 0).

Kết hợp cả hai: compact()trả về bool của không thể phân biệt "không có gì để thu gọn" với " cuộc gọi tóm tắt không thành công" và mã cứng tóm tắt GoogleModel — người dùng Modal/OpenRouter không có GEMINI_API_KEY nhận được sự thất bại thầm lặng mãi mãi. Tất cả các quyết định dưới đây đã được giải quyết rõ ràng với người dùng; các lựa chọn thay thế đã bị đóng lại.

Quyết định

Một đặc điểm, năm quyết định phối hợp:

  1. Điểm cắt hợp lệ: ranh giới theo lượt của người dùng HOẶC bất kỳ ModelResponse ranh giới - quay lại Ranh giới hợp lệ GẦN NHẤT, không bao giờ tới 0. Việc cắt có hiệu lực tại ModelResponse, hoặc tại một ModelRequest mang theo không ToolReturnPart/RetryPromptPart; nó KHÔNG BAO GIỜ hợp lệ theo yêu cầu thực hiện trả lại/thử lại công cụ - lệnh gọi phù hợp của trả lại nằm ngay trước đó ModelResponse, do đó cắt AT a ModelResponse giữ nguyên mọi cặp cuộc gọi/kết quả. Cái này xác định lại Ranh giới nén (sửa đổi ADR-0006 §5) và sửa lỗi nén vi mô miễn phí (§3a không thay đổi về cơ chế - nó kế thừa split_tail). Lịch sử sau nén vẫn mở với bản tóm tắt ModelRequest, do đó, một cái đuôi bắt đầu từ một ModelResponse luôn đi trước một tin nhắn của người dùng.
  2. Nguồn token: được điền cuối cùng ModelResponse.usage của chân (sửa đổi ADR-0006 §3's đo lường hệ thống ống nước; giá trị toán học dự trữ tương đối cửa sổ). Sau mỗi chân, _last_input_tokens = đi bộ run.all_messages() ngược về phần đầu tiên ModelResponse với usage.input_tokens > 0; giá trị = usage.input_tokens + usage.cache_read_tokens (lời nhắc được lưu trong bộ nhớ đệm token vẫn chiếm bối cảnh). Không tìm thấy → 0 và dự phòng an toàn của §3 (0 ⇒ không có cháy cấp độ) là được giữ lại. Máy đo và bộ kích hoạt tiếp tục đọc cùng một số - nguồn sự thật duy nhất được bảo tồn.
  3. compact() trả về ba giá trị CompactOutcome — COMPACTED / NOTHING_TO_COMPACT / SUMMARIZER_FAILED - thay vì bool (sửa đổi câu chuyện phản hồi của ADR-0006 §7). /compact bản in một dòng thân thiện riêng biệt cho mỗi kết quả (SUMMARIZER_FAILED tên .decode/logs/decode.log; thành công vẫn được hiển thị trong sự kiện); đường dẫn tự động ghi lại MỘT dòng THÔNG TIN khi trình kích hoạt được kích hoạt nhưng đã thực hiện không phải đất (outcome != COMPACTED; tương tự như vậy được bắn vi mô nhưng không bị trượt). Suy thoái-không-phá vỡ là không thay đổi - thất bại không bao giờ làm gián đoạn bước ngoặt.
  4. Hạt giống đo sau khi nén: ước tính ký tự≈/4 của hạt mới [summary, *tail]. Bật COMPACTED, _last_input_tokens được đặt thành ước tính để chân trang giảm xuống ngay lập tức; cái số có thẩm quyền của nhà cung cấp chặng tiếp theo sẽ ghi đè lên nó. Điều này cố tình làm mềm ADR-0006 "ước tính không bao giờ kích hoạt" (Hậu quả, dấu đầu dòng cỡ đuôi) thành "ước tính không bao giờ TUYỆT VỜI trình kích hoạt": sau khi nén, nó chỉ có thể giảm bớt, ngắn gọn và được thay thế ở chặng tiếp theo - nó không thể bắt đầu vòng nén.
  5. Trình tóm tắt sử dụng Đường nối nhà cung cấp hiện tại — cơ chế tối thiểu, cả ba nhà cung cấp. context/compaction.py làm rơi nó Settings → GoogleModel chi nhánh; summarize_for_compaction chấp nhận một xây dựng pydantic_ai.models.Modelvà MỘT địa điểm nối dây (tui/app.py - đã xác minh: runtime/flow.py dây không AgentTurnHandler, do đó không có tầng headless nào tồn tại để tua lại) trao cho nó mô hình được xây dựng của nhà cung cấp ACTIVE (factory._build_model đầu ra hoặc của agent được xây dựng mẫu mã riêng - cùng một đường may; hướng nhập khẩu giữ nguyên tui → {agent, context}). các Đường dẫn phiên bản mô hình vẫn là đường nối không có mạng của thử nghiệm. Không có trình tóm tắt giữa các nhà cung cấp núm ghi đè - nhu cầu thực sự thứ hai sẽ biện minh cho nhu cầu đó. Điều gì sẽ biện minh cho việc xem lại: a đo lường nhu cầu về mô hình tóm tắt chuyên dụng rẻ hơn cho mỗi nhà cung cấp.

Không phải mục tiêu (được người dùng phê duyệt): đường dẫn dừng có dấu nhắc trống vẫn không được nén; headless nén chặt; memory/extract.pycặp song sinh của Gemini được mã hóa cứng _resolve_model (lớp lỗi liền kề, cơ chế riêng biệt - nhiệm vụ trong tương lai).

Sơ đồ

flowchart TB
    subgraph leg["One agent leg (agent.iter) — pydantic-ai 1.95.1"]
        msgs["run.all_messages()"]
        lastu["LAST ModelResponse with populated usage<br/>value = input_tokens + cache_read_tokens<br/>(RunUsage cumulative sum REJECTED — ~N× overcount)"]
    end
    msgs --> lastu
    lastu -->|"_last_input_tokens"| gauge["TUI Context Gauge ○◔◑◕●<br/>same number, same window"]
    lastu --> trig{"would-stop trigger<br/>tokens ≥ window·(1-reserve)?"}

    trig -->|"≥ full level or /compact"| split["split_tail — valid Compaction Boundary:<br/>user-turn ModelRequest OR any ModelResponse<br/>NEVER a request carrying ToolReturn/RetryPrompt<br/>snap-back → NEAREST valid boundary (not 0)"]
    trig -->|"≥ micro level"| micro["microcompact (no-LLM, in-memory)<br/>same split_tail → now elides inside one long turn"]
    trig -->|"below / 0 tokens"| noop["no-op"]

    split --> summ["summarize_for_compaction(model)<br/>model = ACTIVE provider via Provider Seam<br/>gemini · openrouter · modal (GoogleModel hardcode DELETED)"]
    seam["agent/factory._build_model()"] -->|"built Model, wired in tui/app"| summ
    summ --> outcome{"CompactOutcome"}
    outcome -->|"COMPACTED"| apply["history = [summary, *tail] · JSONL compaction line<br/>gauge := chars≈/4 estimate of kept history<br/>(understates only; next leg overwrites)"]
    outcome -->|"NOTHING_TO_COMPACT"| l1["/compact: 'nothing to compact yet'"]
    outcome -->|"SUMMARIZER_FAILED"| l2["/compact: names .decode/logs/decode.log<br/>auto: ONE INFO line (trigger fired, not landed)"]
    apply --> ev["ContextCompacted event → TUI"]
    micro --> evm["ContextMicrocompacted (elided > 0)<br/>elided == 0 → ONE INFO line"]

    classDef fix fill:#6a1b9a,stroke:#4a148c,color:#ffffff;
    classDef meas fill:#1565c0,stroke:#0d47a1,color:#ffffff;
    classDef seamc fill:#e65100,stroke:#bf360c,color:#ffffff;
    classDef out fill:#2e7d32,stroke:#1b5e20,color:#ffffff;
    classDef ui fill:#00838f,stroke:#005662,color:#ffffff;
    classDef plain fill:#eceff1,stroke:#607d8b,color:#102027;
    class split,micro fix;
    class msgs,lastu,trig meas;
    class summ,seam seamc;
    class outcome,apply,ev,evm out;
    class gauge,l1,l2 ui;
    class noop plain;

hậu quả

  • Tính năng nén hiện hoạt động dựa trên khối lượng công việc mà nó tồn tại: một lần kích hoạt nặng bằng công cụ dài tại tỷ lệ sử dụng thực sự (mức sử dụng của phản hồi cuối cùng) và tìm ra điểm cắt (ranh giới ModelResponse), cho CẢ HAI bậc. Việc ghép nối có khả năng chịu tải - chỉ sửa các điểm cắt sẽ kích hoạt ~N× quá sớm; chỉ sửa chữa nguồn token sẽ kích hoạt chính xác và sau đó không tìm thấy vết cắt nào.
  • Máy đo ngừng đếm quá ~N×, do đó màu sắc và kích hoạt của nó phù hợp với những gì nhà cung cấp thực sự lập hóa đơn cho mỗi yêu cầu; input_tokens == 0 giữ nguyên ý nghĩa "không bắn".
  • /compact trở nên trung thực: ba kết quả, ba thông điệp; một trình tóm tắt thất bại chỉ ra người dùng vào nhật ký thay vì giả dạng là "không có gì để thu gọn". Lỗi tự động để lại một THÔNG TIN vụn bánh mì.
  • Bất kỳ nhà cung cấp nào cũng có thể tóm tắt — thiết lập chỉ dành cho Modal/OpenRouter không cần khóa Google; sự nén chặt sử dụng mô hình đã phục vụ phiên. context/compaction.py mất đi hàng nhập khẩu từ Google; nhánh Cài đặt của nó _resolve_model bị xóa (đường nối thử nghiệm phiên bản Model vẫn giữ nguyên).
  • Hợp đồng ước tính bị suy yếu một cách có chủ ý: từ "không bao giờ kích hoạt" đến "không bao giờ thổi phồng nó" — sau khi nén nó chỉ có thể diễn đạt ngắn gọn, ngắn gọn cho đến chặng tiếp theo báo cáo. Các tài liệu yêu cầu hợp đồng mạnh hơn được cập nhật.
  • Thử nghiệm rời bỏ: mọi khẳng định trên compact()'s bool quay trở lại và tiếp tục Settings-điều khiển cập nhật xây dựng tóm tắt; một hồi quy capstone ghim hình dạng phiên thất bại ban đầu (1 lời nhắc + hàng tá thông báo công cụ) nên lớp lỗi không thể âm thầm quay lại.
  • Được giữ lại từ ADR-0006 không thay đổi: dự trữ tương đối theo cửa sổ và bất biến của chúng, JSONL lưu trữ điểm kiểm tra (§1), tính chất trong bộ nhớ/không bao giờ tồn tại của micro (cơ chế §3a), bản tóm tắt bộ xương (§4), thuộc tính phần đầu hợp nhất miễn phí (§5), bất biến nhất quán (§6), bộ nhớ nén (§8), cửa sổ/màu sắc của máy đo (§9).
  • Rủi ro được chấp nhận: bây giờ một cái đuôi có thể bắt đầu bằng một ModelResponse - luôn đi trước phần tóm tắt ModelRequest, để lịch sử nhà cung cấp luôn được định dạng tốt; cache_read_tokens ngữ nghĩa khác nhau tùy theo nhà cung cấp (được tính là công suất sử dụng - thận trọng theo hướng an toàn).
Mục lục nguồn