← opencode report

T7 — Overflow detection (cache-aware)

Đếm token đúng — bao gồm cả cache.readcache.write — để phát hiện nguy cơ context overflow trước khi API call thất bại.
Nhóm: B — Context ManagementFile: session/overflow.tsLines: 1–26Status: Stable

Tổng quan Context

Tại sao quan trọng. Với Anthropic models, cache tokens (cache_read_input_tokenscache_creation_input_tokens) vẫn được tính vào context window. Nếu chỉ đếm input_tokens thuần, agent sẽ bị overflow đột ngột khi cache tokens tích lũy lớn — gây lỗi 400 context_length_exceeded giữa chừng.

opencode giải quyết bằng 3 hàm nhỏ gọn trong session/overflow.ts: computeUsable tính số token có thể dùng sau khi trừ buffer output, totalContextTokens cộng tất cả loại token (input + cache.read + cache.write), và shouldCompact quyết định có cần compaction chưa.

Buffer output mặc định là 20 000 token (DEFAULT_RESERVED_OUTPUT_TOKENS). Con số này đảm bảo model luôn có đủ không gian để sinh response, không bị cắt giữa câu. Khi totalContextTokens >= usable, T8 (compaction) được kích hoạt.

Điểm khác biệt. Hầu hết harness (Aider, LangChain) chỉ đếm input_tokens. opencode là một trong số ít harness tính đầy đủ cả 3 loại token — khớp với cách Anthropic tính giới hạn context window.
Context window thực tế. Claude Sonnet 3.5 có context window 200 000 token. Nếu cache.write = 80 000 và input = 40 000, tổng là 120 000 — usable còn 80 000 (với 200k model, reserve 20k). Agent cần biết điều này để không bị bất ngờ.

Phân tích code chi tiết Anatomy

Bước 1 — Tính usable tokens

computeUsable trừ đi reserved buffer khỏi model limit. Math.max(0, ...) xử lý edge case khi model limit nhỏ hơn reserve (ví dụ: model thử nghiệm với context 8k nhưng reserve 20k).

session/overflow.ts — computeUsable

TS
{`
export const DEFAULT_RESERVED_OUTPUT_TOKENS = 20_000

export function computeUsable(
  modelLimit: number,
  reserved = DEFAULT_RESERVED_OUTPUT_TOKENS
): number {
  return Math.max(0, modelLimit - reserved)
  // Math.max(0, ...) đảm bảo không trả về số âm
  // khi reserved > modelLimit (edge case với model nhỏ < 20k)
}
`}

Bước 2 — Đếm cache-aware tokens

Điểm khác biệt quan trọng nhất: cộng cả cache?.readcache?.write vào tổng. Anthropic tính cả hai vào context window. Optional chaining (?.) đảm bảo tương thích với provider không có cache (OpenAI, Groq) — khi đó cache là undefined, fallback về 0.

session/overflow.ts — totalContextTokens

TS
{`
export type Usage = {
  input?: number
  cache?: { read?: number; write?: number }
  output?: number
}

export function totalContextTokens(usage: Usage): number {
  return (
    (usage.input  ?? 0) +
    (usage.cache?.read  ?? 0) +
    (usage.cache?.write ?? 0)
    // NOTE: output tokens KHÔNG được tính vào đây —
    // output không chiếm context window của lần gọi tiếp theo
  )
}
`}

Bước 3 — Quyết định compact

shouldCompact kết hợp hai hàm trên thành 1 boolean đơn giản. Hàm này được gọi sau mỗi LLM response trong T1 agent loop.

session/overflow.ts — shouldCompact

TS
{`
export function shouldCompact(
  usage: Usage,
  modelLimit: number,
  reserved?: number          // optional, default = 20_000
): boolean {
  const usable = computeUsable(modelLimit, reserved)
  const used   = totalContextTokens(usage)
  return used >= usable
  // >= (không phải >) để trigger compact sớm hơn 1 token
  // tránh off-by-one race condition
}
`}

Luồng gọi trong agent loop

Hàm này được gọi ngay sau khi nhận usage từ response streaming, trước khi xử lý tool calls. Nếu shouldCompact trả về true, session sẽ ngắt tool execution và đi vào T8.

Tương tác với các kỹ thuật khác Interaction

T7 là sensor — nó không thay đổi gì, chỉ đọc và quyết định. Kết quả của nó kích hoạt hoặc ảnh hưởng đến nhiều kỹ thuật khác trong nhóm B.

┌─────────────────────────────────────────────────────────┐ │ T1 — Agent Loop │ │ │ │ LLM response ──→ extract usage ──→ T7.shouldCompact? │ │ │ │ │ ┌───────┴───────┐ │ │ YES NO │ │ │ │ │ │ ▼ ▼ │ │ T8 Compaction continue │ │ (triggers T9, │ │ T12, then T11) │ └─────────────────────────────────────────────────────────┘ T7 nhận input từ: ← LLM API usage object (input_tokens, cache_read/write) ← modelLimit từ provider config (T23/T24) T7 ảnh hưởng đến: → T8: trigger compaction khi shouldCompact = true → T10: cache.write tokens ảnh hưởng đến sizing system prompt

Ngoài ra, T7 gián tiếp ảnh hưởng T10 (cache-aware system prompt): khi cache.write tăng đột biến (system prompt thay đổi → cache miss), T7 sẽ báo động sớm hơn, nhắc nhở cần tối ưu T10 để giảm cache churn.

Timing quan trọng. T7 chạy sau mỗi LLM call, không phải trước. Điều này có nghĩa session sẽ gọi LLM ít nhất 1 lần trước khi phát hiện overflow — hợp lý vì không thể biết usage thực tế cho đến khi API trả về.

Failure modes Risk

FM-1: Không đếm cache tokens

Cách triển khai naïve phổ biến nhất — chỉ dùng input_tokens:

Anti-pattern — thiếu cache tokens

TS
{`
// ❌ Naive: bỏ qua cache tokens
function shouldCompact(usage, modelLimit) {
  return usage.input_tokens >= modelLimit - 20_000
  // Khi cache.write = 80k và input = 40k:
  //   naive thấy 40k < 180k → không compact
  //   thực tế 120k < 180k → cũng ok lần này
  //   nhưng lần sau khi input tăng → đột ngột 400 error
}
`}

Hệ quả: agent hoạt động bình thường cho đến khi input_tokens tăng đủ lớn, khi đó API trả về lỗi 400 mà không có cơ hội compact trước.

FM-2: Reserve buffer quá nhỏ

Nếu reserve là 4 096 (mặc định của một số harness), với model 200k: usable = 195 904. Session sẽ compact muộn hơn. Khi LLM cần sinh response dài (chain-of-thought), 4k output space không đủ → response bị truncate, gây tool call malformed JSON.

Anti-pattern — reserve quá nhỏ

TS
{`
// ❌ Reserve nhỏ — response bị truncate
const RESERVE = 4_096   // quá thấp cho models dùng CoT dài

// opencode chọn 20_000 — đủ cho hầu hết response dài
// bao gồm cả cases model sinh XML tool call + explanation
`}

FM-3: Không xử lý provider không có cache

Với OpenAI hoặc Groq, usage.cacheundefined. Nếu không dùng optional chaining, hàm sẽ throw TypeError: Cannot read properties of undefined. opencode dùng usage.cache?.read ?? 0 để safe.

Ưu điểm
  • Phát hiện overflow sớm — trước khi API lỗi
  • Cache-aware — chính xác với Anthropic models
  • Optional chaining — tương thích multi-provider
  • Reserve 20k — đủ cho output dài (CoT, XML tool calls)
Nhược điểm
  • Reserve cứng 20k — không dynamic theo model/task
  • Chỉ trigger sau LLM call — không predict trước
  • Không tính output_tokens của turn trước vào projection

So sánh với các harness khác Compare

HarnessCách đếm tokenCache-aware?Reserve buffer
opencodeinput + cache.read + cache.write20 000 token cứng
Claude CodeTương tự (cùng Anthropic)Không public chi tiết
Aidertiktoken approximate, chỉ inputKhông~1 000 token (configurable)
LangChainConversationTokenBufferMemory — chỉ message tokensKhôngmax_token_limit tuỳ config
Lưu ý Aider. Aider dùng tiktoken để estimate token count client-side — nhanh hơn nhưng không chính xác 100% với tokenizer thực tế của mỗi model. Với Anthropic models, sai lệch thường 5–10%.

Implementation recipe Recipe

Triển khai cache-aware overflow detection với per-provider switch:

overflow.ts — implementation recipe

TS
{`
// overflow.ts — cache-aware token overflow detection

export const DEFAULT_RESERVED_OUTPUT_TOKENS = 20_000

export type Usage = {
  input?:  number
  output?: number
  cache?: {
    read?:  number
    write?: number
  }
}

export type Provider = 'anthropic' | 'openai' | 'groq' | 'google'

/** Token nào chiếm context window, theo provider */
export function totalContextTokens(
  usage: Usage,
  provider: Provider = 'anthropic'
): number {
  const base = usage.input ?? 0

  // Anthropic: cache tokens chiếm context window
  if (provider === 'anthropic') {
    return base
      + (usage.cache?.read  ?? 0)
      + (usage.cache?.write ?? 0)
  }

  // OpenAI/Groq/Google: không có prompt caching
  // (hoặc caching transparent — không ảnh hưởng limit)
  return base
}

export function computeUsable(
  modelLimit: number,
  reserved = DEFAULT_RESERVED_OUTPUT_TOKENS
): number {
  return Math.max(0, modelLimit - reserved)
}

export function shouldCompact(
  usage:      Usage,
  modelLimit: number,
  opts?: { reserved?: number; provider?: Provider }
): boolean {
  const usable = computeUsable(modelLimit, opts?.reserved)
  const used   = totalContextTokens(usage, opts?.provider)
  return used >= usable
}
`}

Tham khảo Refs