← opencode report

T27 — Model-specific system prompt dispatch + dynamic env

4 prompt template (anthropic / gpt / gemini / beast) dispatch theo model regex. Env block ... inject fresh mỗi call: git status, cwd, platform, available skills.
Nhóm: F — System PromptFile: session/system.ts · Lines 19–77ID: F.1 / T27Status: Stable

Tổng quan System Prompt

Tại sao quan trọng. Claude, GPT-4, Gemini có "phong cách" khác nhau: Claude phản hồi tốt với XML tags và explicit structure; GPT-4/o1 cần instructions ngắn gọn hơn; Gemini có quirks với system message. Một prompt chung cho tất cả → performance lowest common denominator. Dispatch theo model family cho phép tối ưu từng path. Env block dynamic (git status, working dir, platform) đảm bảo agent luôn biết bối cảnh thực tế ngay lúc đó — không phụ thuộc vào context lỗi thời từ turns trước.
Skills list permission-gated: renderSkills(ctx.skills, { permissionGate: ctx.perm }) — model chỉ thấy skills mà permission hiện tại cho phép. Agent không biết có thêm capabilities mà nó không được dùng trong context này.

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

Model family detection và template dispatch

session/system.ts — pickTemplate() + buildSystem()

TS
import PROMPT_ANTHROPIC from "./prompts/anthropic.txt"
import PROMPT_GPT       from "./prompts/gpt.txt"
import PROMPT_BEAST     from "./prompts/beast.txt"   // gpt-4 | o1 | o3
import PROMPT_GEMINI    from "./prompts/gemini.txt"
import PROMPT_DEFAULT   from "./prompts/default.txt"

function pickTemplate(modelId: string): string {
  if (/^(gpt-4|o1|o3)/i.test(modelId)) return PROMPT_BEAST   // o1-style reasoning models
  if (/^gpt/i.test(modelId))            return PROMPT_GPT
  if (/^gemini/i.test(modelId))         return PROMPT_GEMINI
  if (/claude/i.test(modelId))          return PROMPT_ANTHROPIC
  return PROMPT_DEFAULT
}

export function buildSystem(model: Model, ctx: Ctx): string[] {
  const template = pickTemplate(model.id)

  // Dynamic env block — fresh mỗi call
  const envBlock = `
<env>
  directory: ${ctx.cwd}
  worktree: ${ctx.worktree}
  git: ${ctx.gitStatusShort}
  platform: ${process.platform}
  date: ${new Date().toISOString()}
</env>`

  // Skills list — chỉ show skills được phép dùng
  const skillsBlock = renderSkills(ctx.skills, { permissionGate: ctx.perm })

  // Project instructions từ AGENTS.md / CLAUDE.md (T28)
  const projectBlock = ctx.agentsFile?.content ?? ""

  // Trả về array → AI SDK dùng multi-part system message
  // [template, env] cache-stable, [skills, project] dynamic
  return [template, envBlock, skillsBlock, projectBlock]
}
model.id = "claude-opus-4" │ ▼ pickTemplate("claude-opus-4") │ → /claude/i matches ▼ PROMPT_ANTHROPIC (cached, stable) │ + envBlock (dynamic, per-call) │ <env> │ directory: /Users/nghia/project │ git: 3 modified files, 1 untracked │ platform: darwin │ </env> │ + skillsBlock (permission-gated) │ Available skills: bash, edit, read, grep │ [task tool hidden if permission denied] │ + projectBlock (AGENTS.md content, T28) │ <project-instruction src="./AGENTS.md"> │ Always write tests. Use TypeScript strict. │ </project-instruction> │ ▼ buildSystem() returns: [template, env, skills, project] │ ▼ AI SDK: { role: "system", content: [...parts] }

Generic model-dispatch pattern

TS
const PROMPTS: Record<string, string> = {
  anthropic: ANTHROPIC_TEMPLATE,
  openai:    OPENAI_TEMPLATE,
  gemini:    GEMINI_TEMPLATE,
}

function detectFamily(model: string): string {
  if (/claude/i.test(model))    return "anthropic"
  if (/gpt|o1|o3/i.test(model)) return "openai"
  if (/gemini/i.test(model))    return "gemini"
  return "openai"  // reasonable default
}

function buildSystemPrompt(model: string, ctx: Ctx): string {
  const template = PROMPTS[detectFamily(model)] ?? PROMPTS.openai
  const env = `<env>cwd=${ctx.cwd}; git=${ctx.git}; os=${process.platform}</env>`
  return [template, env, ctx.projectInstructions ?? ""].join("\n\n")
}

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

  • T10 (Cache-aware prompt structure): Template (static) và env (dynamic) là 2 parts riêng. Template part được Anthropic cache. Env part thay đổi mỗi call → invalidate env cache, nhưng template cache vẫn còn. Tách 2 parts giúp tối đa cache hit rate.
  • T28 (AGENTS.md cascading): projectBlock là kết quả của T28 — findUp từ cwd để tìm AGENTS.md/CLAUDE.md. T27 gọi T28 và nhận content để append vào system prompt.
  • T13 (Tool description .txt): Tool description cũng dùng .txt file — cùng pattern. System prompt template là macroscopic version của cùng technique: tách text khỏi code.

Failure modes Failure

1. Template drift giữa model families

Khi update policy (vd thêm rule mới về tool use), phải update tất cả 4–5 template files. Dễ miss 1 file → model family đó hoạt động theo policy cũ. Cần process review để sync templates.

2. Regex detect model không bền

Model ID mới không match regex (claude-3-7-sonnet-20251022 OK, nhưng nếu Anthropic đặt tên mới không có "claude" → PROMPT_DEFAULT thay vì PROMPT_ANTHROPIC). Cần fallback explicit hoặc mapping table.

3. Env block invalidate cache

Env block thay đổi mỗi call (git status, date) → part thứ 2 không được cache. Cần cân nhắc: đặt env block sau template (không trước) để tối đa cache hit cho static parts trước env.

Cache ordering: Anthropic cache tính từ đầu system prompt. Đặt static template trước env block → cache hit cho template. Đặt env trước template → cache miss mọi lần.

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

HarnessModel-specific promptDynamic envSkills gating
opencode4 templates, regex dispatch✅ mỗi call✅ permission-gated
Claude CodeSingle template (Anthropic only)N/A
Aider1 template, model-agnostic⚠️ partial (cwd only)
Cline1 template per provider config⚠️ cwd + date
OpenHarness1 template cho Anthropic⚠️ minimal

Implementation recipe Recipe

TS
// 1. Create template files
// prompts/claude.txt, prompts/openai.txt, prompts/gemini.txt
import CLAUDE_TEMPLATE  from "./prompts/claude.txt"
import OPENAI_TEMPLATE  from "./prompts/openai.txt"
import GEMINI_TEMPLATE  from "./prompts/gemini.txt"
import DEFAULT_TEMPLATE from "./prompts/default.txt"

// 2. Model family detection table
const FAMILY_MAP: Array<[RegExp, string]> = [
  [/claude/i,           "claude"],
  [/gpt|o1|o3/i,        "openai"],
  [/gemini/i,           "gemini"],
  [/llama|mistral/i,    "default"],
]

const TEMPLATES: Record<string, string> = {
  claude:  CLAUDE_TEMPLATE,
  openai:  OPENAI_TEMPLATE,
  gemini:  GEMINI_TEMPLATE,
  default: DEFAULT_TEMPLATE,
}

function getTemplate(modelId: string): string {
  for (const [re, family] of FAMILY_MAP) {
    if (re.test(modelId)) return TEMPLATES[family]
  }
  return TEMPLATES.default
}

// 3. Build system prompt with fresh env
interface Ctx {
  cwd:     string
  git:     string
  skills:  string[]
  project: string | null
}

function buildSystem(modelId: string, ctx: Ctx): string[] {
  const template = getTemplate(modelId)

  const env = [
    "<env>",
    `  cwd: ${ctx.cwd}`,
    `  git: ${ctx.git}`,
    `  platform: ${process.platform}`,
    `  date: ${new Date().toISOString().slice(0, 10)}`,
    "</env>",
  ].join("\n")

  const skills = ctx.skills.length
    ? `Available skills:\n${ctx.skills.map(s => `- ${s}`).join("\n")}`
    : ""

  return [template, env, skills, ctx.project ?? ""].filter(Boolean)
}

Tham khảo Refs