← opencode report

T9 — Tool output pruning với protected tools

Trước khi compact, thay thế nội dung tool outputs bằng — nhưng bảo vệ skill tool và 2 user turn cuối để giữ nguyên runtime instructions.
Nhóm: B — Context ManagementFile: session/compaction.tsLines: 171–219Status: Stable

Tổng quan Context

Tại sao quan trọng. Tool outputs thường rất dài — đọc file 500 dòng, kết quả grep 200 match, JSON response 10KB. Khi compact, gửi toàn bộ những output này cho LLM summary là lãng phí: LLM sẽ không tóm tắt tốt hơn thực tế. T9 giải quyết bằng cách thay thế chúng bằng placeholder trước khi gửi cho LLM compact, giảm đáng kể số token trong head.

Điểm tinh tế là protected tools: tool skill không được prune vì output của nó là runtime instructions (SKILL.md content) mà LLM cần đọc để biết cách hành xử. Prune skill output đồng nghĩa với xoá hướng dẫn của agent — không thể phục hồi từ summary.

Ngoài ra, T9 tôn trọng skip boundary: 2 user turn cuối không được prune bất kể tool nào. Đây là vùng "hot context" chứa lệnh gần nhất và kết quả trực tiếp của nó.

Metadata compacted. Mỗi message bị prune được đánh dấu time.compacted: Date.now(). Điều này cho phép debug và audit: biết chính xác message nào đã bị prune và khi nào.

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

Bước 1 — Xác định protected tools và skip boundary

Protected set được định nghĩa cứng là new Set(["skill"]). Skip boundary = index của user turn thứ 2 tính từ cuối.

session/compaction.ts — setup

TS
{`
const PROTECTED_TOOLS = new Set(["skill"])
// "skill" tool trả về nội dung SKILL.md — runtime instructions
// Prune nó sẽ khiến agent mất hướng dẫn hoạt động

function findSkipBoundary(messages: Message[]): number {
  // Tìm index của user turn thứ 2 từ cuối
  let userTurnCount = 0
  for (let i = messages.length - 1; i >= 0; i--) {
    if (messages[i].role === 'user') {
      userTurnCount++
      if (userTurnCount >= 2) return i  // skip messages từ i trở đi
    }
  }
  return 0  // Ít hơn 2 user turns → không skip gì cả (prune tất cả)
}
`}

Bước 2 — Selective pruning với 2 điều kiện

Một message bị prune khi đồng thời thỏa: (1) role = "tool", (2) tool name không trong protected set, (3) index < skipBoundary.

session/compaction.ts — pruning loop

TS
{`
export function pruneToolOutputs(messages: Message[]): Message[] {
  const skipBoundary = findSkipBoundary(messages)

  return messages.map((msg, idx) => {
    // Không prune: sau skip boundary (gần đây)
    if (idx >= skipBoundary) return msg

    // Không prune: không phải tool message
    if (msg.role !== 'tool') return msg

    // Không prune: protected tool
    const toolName = msg.metadata?.toolName ?? ''
    if (PROTECTED_TOOLS.has(toolName)) return msg

    // Prune: thay content bằng placeholder
    return {
      ...msg,
      content:  '<tool-output-compacted />',
      metadata: {
        ...msg.metadata,
        time: {
          ...msg.metadata?.time,
          compacted: Date.now(),  // audit trail
        },
      },
    }
  })
}
`}

Ví dụ thực tế

Với 10 messages, 2 user turns ở vị trí 3 và 7 (0-indexed): skipBoundary = 3. Messages 0, 1, 2 có thể bị prune. Messages 3–9 được bảo vệ.

Ví dụ — skip boundary visualization

TS
{`
Index  Role    Tool        Action
  0    user    —           (trước boundary)
  1    asst    read_file   (trước boundary)
  2    tool    read_file   ← PRUNE (không protected, trước boundary)
  3    user    —           ← skipBoundary (user turn thứ 2)
  4    asst    skill       (sau boundary → bảo vệ)
  5    tool    skill       ← BẢO VỆ (protected tool)
  6    asst    edit_file   (sau boundary)
  7    tool    edit_file   ← BẢO VỆ (sau boundary)
  8    user    —           (sau boundary)
  9    asst    —           (sau boundary)
`}

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

T8 Compaction bắt đầu │ ▼ ┌────────────────────────────────────────────┐ │ T9 — Tool Output Pruning │ │ (pre-processing step trong T8) │ │ │ │ Input: full messages array │ │ ↓ │ │ 1. findSkipBoundary (last 2 user turns) │ │ ↓ │ │ 2. pruneToolOutputs │ │ - skip: idx >= boundary │ │ - skip: role != 'tool' │ │ - skip: PROTECTED_TOOLS │ │ - replace: content → placeholder │ │ ↓ │ │ Output: pruned messages (smaller) │ └────────────────────────────────────────────┘ │ ▼ T8 tiếp tục: split head/tail trên pruned messages T12 template: compact head (đã nhỏ hơn nhiều) Protected tools ← configured bởi plugin system (T26+)

T9 là bước pre-processing bắt buộc trong T8. Thứ tự đúng: T9 trước, rồi mới split head/tail (T8), rồi LLM compact (T12). Nếu đảo ngược, tail có thể chứa tool messages đã bị prune — gây ra context không nhất quán.

Plugin extensibility. Protected tools list có thể mở rộng qua plugin API. Ví dụ: một plugin custom có tool memory_store trả về long-term memory content → thêm vào protected list để không bị prune.

Failure modes Risk

FM-1: Prune tool output gần nhất

Nếu không có skip boundary, model compact không thấy kết quả tool call vừa thực hiện:

Anti-pattern — prune tất cả tool outputs

TS
{`
// ❌ Prune tất cả — kể cả turn gần nhất
function pruneAll(messages) {
  return messages.map(msg =>
    msg.role === 'tool'
      ? { ...msg, content: '<compacted />' }
      : msg
  )
  // Model nhận lệnh "đọc file X" (user turn) nhưng
  // không thấy kết quả đọc → hỏi lại hoặc làm sai
}
`}

FM-2: Prune skill output

skill tool trả về nội dung SKILL.md — hướng dẫn chi tiết về cách thực hiện một tác vụ. Prune nó khiến agent không còn runtime instructions:

Anti-pattern — prune skill output

TS
{`
// ❌ Không có protected set — prune cả skill
// Kết quả: model không biết cách chạy skill build-report
// → action sai, không theo template
const PROTECTED_TOOLS = new Set()  // empty — nguy hiểm
`}

FM-3: Skip boundary không đủ

Nếu chỉ skip 1 user turn (thay vì 2), một số tool results liên quan đến lệnh gần nhất vẫn có thể bị prune — đặc biệt khi 1 user turn trigger nhiều tool calls liên tiếp.

Ưu điểm
  • Giảm đáng kể số token trong head trước khi compact
  • Protected tools — bảo vệ runtime instructions
  • Skip boundary = 2 turns — giữ đủ hot context
  • time.compacted metadata — audit trail
Nhược điểm
  • Protected list cứng — plugin phải tự extend
  • Placeholder đơn giản — không biết output đã prune là gì
  • Không prune partially — prune toàn bộ content hoặc không

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

HarnessTool output pruning?Protected tools?Skip boundary?
opencodeCó — selective với placeholderCó (skill + extensible)2 user turns cuối
Claude CodeKhông rõ chi tiếtCó (không public)
LangChainKhông — prune đều tất cả hoặc khôngKhông có khái niệmKhông
CursorKhông public chi tiếtKhông rõKhông rõ

Implementation recipe Recipe

compaction.ts — selective tool pruner recipe

TS
{`
// tool-pruner.ts — selective pruning với protected set + boundary

const PROTECTED_TOOLS = new Set<string>(["skill"])
const SKIP_BOUNDARY_TURNS = 2
const COMPACTED_PLACEHOLDER = '<tool-output-compacted />'

export function pruneToolOutputs(
  messages:       Message[],
  protectedTools: Set<string> = PROTECTED_TOOLS
): Message[] {
  // Tìm skipBoundary: user turn thứ N từ cuối
  let userTurnCount = 0
  let skipBoundary  = 0

  for (let i = messages.length - 1; i >= 0; i--) {
    if (messages[i].role === 'user') {
      userTurnCount++
      if (userTurnCount >= SKIP_BOUNDARY_TURNS) {
        skipBoundary = i
        break
      }
    }
  }

  return messages.map((msg, idx) => {
    // Giữ nguyên: sau boundary, không phải tool, hoặc protected
    if (idx >= skipBoundary)                           return msg
    if (msg.role !== 'tool')                           return msg
    if (protectedTools.has(msg.metadata?.toolName ?? '')) return msg

    // Prune với audit metadata
    return {
      ...msg,
      content: COMPACTED_PLACEHOLDER,
      metadata: {
        ...msg.metadata,
        time: { ...msg.metadata?.time, compacted: Date.now() },
      },
    }
  })
}
`}

Tham khảo Refs