← opencode report

T5 — Synthetic system reminder cho mid-turn messages

Khi user gõ thêm message trong lúc agent đang chạy multi-step, opencode wrap text đó trong tags ở step > 1 — nhấn mạnh để model không bỏ qua input mới lẫn trong history.
Nhóm: A — Agent Loop & StreamingFile: session/prompt.tsLines: 1453–1468ID: A.5

Tổng quan Overview

Trong một agent session dài, user có thể gõ thêm message trong khi agent đang xử lý multi-step task. Ví dụ: agent đang đọc và sửa file, user nhắn thêm "hãy thêm comment tiếng Việt cho tất cả function". Nếu message này đi vào history như một user message bình thường, model có thể không nhận ra rằng đây là instruction mới cần xử lý ngay.

opencode giải quyết bằng cách transform text của user message mới thành một block được bọc trong <system-reminder>...</system-reminder>. XML tag này signal cho model rằng đây là instruction ưu tiên, cần được address trước khi tiếp tục. Kỹ thuật này chỉ áp dụng ở step > 1 (step 1 là initial prompt, không cần reminder).

Tại sao quan trọng: LLM attention mechanism có thể "bị kéo" về phía context đầu và giữa conversation — message mới trong history dài dễ bị underweighted. Wrap trong XML tag là signal rõ ràng hơn, tốn ít token hơn so với thêm message riêng, và không làm phức tạp message structure.
Kỹ thuật tương tự ở Anthropic: Blog "Claude Code auto mode" của Anthropic đề cập đến system reminder pattern. Đây là confirmation rằng Claude được train để "pay attention" đặc biệt đến content trong các XML tags như <system-reminder>.
Giới hạn: Wrap text không đảm bảo model sẽ ACT on it. Model có thể acknowledge ("tôi hiểu yêu cầu mới") rồi tiếp tục task cũ. Đây là giải pháp soft — không phải hard interrupt như task cancellation.

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

Điều kiện áp dụng reminder

Ba điều kiện phải thỏa đồng thời để wrap được áp dụng:

session/prompt.ts:1453-1468 — synthetic reminder injection

TS
{`
// Chỉ áp dụng từ step 2 trở đi
if (step > 1 && lastFinished) {
  for (const m of msgs) {
    // Điều kiện 1: phải là user message
    if (m.info.role !== "user") continue

    // Điều kiện 2: phải là message MỚI (sau lần loop cuối hoàn thành)
    if (m.info.id <= lastFinished.id) continue

    for (const p of m.parts) {
      // Điều kiện 3: chỉ wrap text, không wrap media/synthetic/ignored
      if (p.type !== "text" || p.ignored || p.synthetic) continue
      if (!p.text.trim()) continue

      // Wrap trong system-reminder XML block
      p.text = [
        "<system-reminder>",
        "The user sent the following message:",
        p.text,
        "",
        "Please address this message and continue with your tasks.",
        "</system-reminder>",
      ].join("\n")
    }
  }
}
`}

lastFinished — tracking loop state

lastFinished là reference đến message cuối cùng đã được process trong iteration trước. Bất kỳ user message nào có id > lastFinished.id là message mới — chưa được model "see" trong context tự nhiên.

lastFinished tracking

TS
{`
// Được update mỗi lần kết thúc một step:
lastFinished = lastAssistant  // assistant response cuối cùng

// Mỗi iteration: user messages mới (id > lastFinished.id)
// là những message đến trong khi loop đang chạy
const newUserMsgs = msgs.filter(
  m => m.role === "user" && m.id > lastFinished.id
)
`}

Chỉ wrap text — không wrap media hay synthetic parts

Điều kiện p.type !== "text" || p.ignored || p.synthetic bảo vệ các loại part khác: image attachments, file references, và các synthetic messages được inject bởi harness (vd: compaction continue message).

Part type filtering

TS
{`
// Các loại part có thể có trong một user message:
// - type: "text"      → ← CHỈ WRAP type này
// - type: "image"     → skip (không wrap media)
// - type: "file"      → skip
// - synthetic: true   → skip (harness-injected, vd compaction)
// - ignored: true     → skip (user đã mark là irrelevant)
`}

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

T5 chạy trong "build messages" phase của mỗi T1 iteration: T1 loop (iteration N, step > 1): │ ├─ build messages phase │ │ │ ├─ filterCompactedMessages() ← lọc messages đã compact │ │ │ ├─ T5: inject system reminders ← CHỖ NÀY │ │ │ │ │ └─ user messages mới (id > lastFinished.id) │ │ → wrap text trong <system-reminder> │ │ │ └─ inject system prompt (T32/T33: model-specific dispatch) │ └─ gọi LLM với messages đã transform │ └─ T2: stream demux → T3 → T4 → ... Timeline message IDs: [msg 1: user "viết hàm X"] [msg 2: assistant "..."] [msg 3: tool_call] [msg 4: tool_result] ← step 1 hoàn thành, lastFinished = msg 4 → [msg 5: user "thêm comment"] ← step 2: id 5 > lastFinished.id 4 → WRAP

T5 chạy trước khi request được gửi đến LLM, sau khi messages đã được filter bởi compaction logic. Nó không tương tác trực tiếp với T2-T4 (streaming), nhưng ảnh hưởng đến quality của model response trong iteration đó.

Failure modes Failures

Failure 1: User message bị bỏ qua trong long loop

Không có reminder, user message mới lẫn vào history và model tiếp tục task cũ:

Không có reminder — user instruction bị miss

TS
{`
// Step 3, history:
// [user: "viết service X"]
// [assistant: "OK tôi sẽ..."]
// [tool: read_file result]
// [tool: write_file result]
// [user: "hãy dùng TypeScript strict mode"] ← message mới!
// [assistant: ...] ← model có thể không address vì nó "buried"

// Với reminder:
// [user: "<system-reminder>The user sent:\nhãy dùng TypeScript strict mode\nPlease address...</system-reminder>"]
// Model NOW sees it as priority instruction
`}

Failure 2: Wrap ở step 1 — không cần thiết

Nếu áp dụng reminder ngay cả ở step 1, initial user message sẽ bị wrap không cần thiết — overhead token và có thể confuse model về context:

Sai: wrap ở step 1

TS
{`
// WRONG: không check step > 1
for (const m of msgs) {
  if (m.role === "user") wrapWithReminder(m)
}
// Initial user message cũng bị wrap:
// "<system-reminder>The user sent:\nViết hàm sort\nPlease address...</system-reminder>"
// → unneccesary và verbose

// CORRECT:
if (step > 1 && lastFinished) {  // chỉ step > 1
  // ...
}
`}

Failure 3: Wrap media parts — format error

Nếu vô tình wrap image attachment hoặc file content trong system-reminder text, model sẽ nhận string biểu diễn của binary data:

Wrap sai part type

TS
{`
// WRONG: wrap tất cả parts
for (const p of m.parts) {
  p.text = wrapReminder(p.text || p.toString())
  // Image part: p.text = undefined → "undefined" được wrap
}

// CORRECT: chỉ text parts không phải synthetic/ignored
if (p.type !== "text" || p.ignored || p.synthetic) continue
`}

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

HarnessMid-turn message handlingXML wrappingStep-aware
opencodeSystem reminder wrapCó — <system-reminder>Có — chỉ step > 1
Claude CodeCó tương tự (Anthropic blog)Có thể có, không publicKhông rõ
AiderKhông có pattern nàyKhôngKhông
ClineKhông cóKhôngKhông
AutoGPTHuman input chờ turn mớiKhôngKhông

opencode và Claude Code (theo Anthropic documentation) là hai harness duy nhất trong danh sách có explicit mid-turn message handling. Aider và Cline xử lý user input theo turn-based model truyền thống — agent xong mới nhận input mới.

Implementation recipe Recipe

Message transformer function — có thể integrate vào bất kỳ agent loop nào:

system-reminder-injector.ts

TS
{`
interface Message {
  id: string
  role: "user" | "assistant" | "tool"
  parts: MessagePart[]
}

interface MessagePart {
  type: "text" | "image" | "file"
  text?: string
  ignored?: boolean
  synthetic?: boolean
}

function injectSystemReminders(
  messages: Message[],
  lastFinishedId: string | null,
  step: number
): Message[] {
  // Chỉ áp dụng từ step 2 trở đi
  if (step <= 1 || lastFinishedId === null) {
    return messages
  }

  return messages.map((msg) => {
    // Chỉ xử lý user messages
    if (msg.role !== "user") return msg

    // Chỉ xử lý messages MỚI (sau lastFinished)
    if (msg.id <= lastFinishedId) return msg

    // Transform text parts
    const newParts = msg.parts.map((part) => {
      // Bỏ qua non-text, synthetic, ignored, empty
      if (
        part.type !== "text" ||
        part.ignored ||
        part.synthetic ||
        !part.text?.trim()
      ) {
        return part
      }

      // Wrap trong system-reminder
      return {
        ...part,
        text: [
          "<system-reminder>",
          "The user sent the following message:",
          part.text,
          "",
          "Please address this message and continue with your tasks.",
          "</system-reminder>",
        ].join("\n"),
      }
    })

    return { ...msg, parts: newParts }
  })
}

// Usage trong agent loop:
// const processedMessages = injectSystemReminders(
//   rawMessages,
//   lastAssistantMessage?.id ?? null,
//   currentStep
// )
// // Gửi processedMessages cho LLM
`}
Recipe trên là pure function (immutable) — không mutate messages gốc. opencode mutate trực tiếp p.text vì messages đã được clone trong build phase. Cả hai approach đều đúng — immutable version an toàn hơn cho testing.

Tham khảo Refs

Nguồn chính