Series 1 - Bài 4: Agent Core, nơi orchestration xảy ra

Agent Core là trung tâm điều phối của CLI Agent — nơi agentic loop sống, context được build, và tool dispatch xảy ra. Bài này phân tích từng component và sequence diagram của một complete turn.

5 phút đọc
Tập này đang được chuẩn bị, quay lại sau nhé.

Nếu CLI Layer là giao diện và Tool Layer là tay chân, thì Agent Core là não. Nhưng đây là não theo nghĩa traffic controller — không phải nơi business logic sống, mà là nơi điều phối để đúng thứ xảy ra vào đúng thời điểm.

Hiểu sai Agent Core là nguồn gốc của nhiều architecture mistake phổ biến nhất trong agent system.

Một complete turn nhìn từ Agent Core

 User message: "Refactor hàm processLoan trong loan.service.ts"

 Turn bắt đầu:
 ┌─────────────────────────────────────────────────────────┐
 │ 1. ContextBuilder.build()                              │
 │    ├─ Load system prompt                               │
 │    ├─ Load file tree từ Context Layer                  │
 │    ├─ Attach conversation history                     │
 │    └─ Append user message                             │
 │    → messages[] ready                                 │
 │                                                       │
 │ 2. LLMClient.call(messages[])                         │
 │    ├─ POST /v1/messages với tool definitions          │
 │    ├─ Parse SSE stream                                │
 │    └─ Collect response blocks                         │
 │    → response: tool_use | text                       │
 │                                                       │
 │ 3. Response routing:                                  │
 │                                                       │
 │    If tool_use:                                       │
 │    ├─ ToolDispatcher.execute(tool_use)                │
 │    ├─ Append tool_use to messages[]                   │
 │    ├─ Append tool_result to messages[]                │
 │    └─ GOTO step 2 (loop)                              │
 │                                                       │
 │    If text:                                           │
 │    └─ Return text to CLI Layer (turn ends)            │
 └─────────────────────────────────────────────────────────┘

ContextBuilder: Assembling the window

ContextBuilder có một job: tạo ra messages[] array đúng format để gửi đến LLM.

 messages[] structure sau khi ContextBuilder chạy:

 [
   {
     role: "user",
     content: [
       { type: "text", text: "[SYSTEM]\n" + systemPrompt },
       { type: "text", text: "[PROJECT]\n" + fileTree },
     ]
   },
   // ... conversation history ...
   {
     role: "user",
     content: "Refactor hàm processLoan..."
   }
 ]

Thứ tự trong messages[] quan trọng. LLM attend mạnh hơn vào phần đầu và phần cuối. System prompt và current user message nên ở những vị trí này.

ContextBuilder cũng là nơi enforce token budget. Trước khi build, nó estimate tổng token count. Nếu quá limit, nó trim conversation history cũ — nhưng không bao giờ trim system prompt hay current message.

 Token budget strategy:

 Priority 1 (không bao giờ trim): System prompt
 Priority 2 (không bao giờ trim): Current user message  
 Priority 3 (trim nếu cần):      Recent tool results
 Priority 4 (trim đầu tiên):     Old conversation history

LLMClient: Thin HTTP wrapper

LLMClient cố tình thin. Không có business logic, không có retry với backoff phức tạp, không có caching. Một job: gọi API và parse response.

 LLMClient.call(messages[]):

 1. Serialize request:
    {
      model: "claude-sonnet-4-5",
      max_tokens: 8096,
      stream: true,
      tools: [...tool definitions...],
      messages: messages[]
    }

 2. POST đến /v1/messages

 3. Parse SSE stream:
    event: content_block_start   → bắt đầu block mới
    event: content_block_delta   → thêm chunk vào block
    event: content_block_stop    → block hoàn thành
    event: message_stop          → response hoàn thành

 4. Collect và return:
    [
      { type: "text", text: "..." },
      { type: "tool_use", name: "read_file", input: {...} }
    ]

Streaming là bắt buộc cho UX. Nếu không stream, user phải đợi toàn bộ response trước khi thấy gì. Với response dài 2000 words, đó có thể là 5-10 giây chờ trong im lặng.

Với streaming, user thấy từng word xuất hiện — trải nghiệm responsive hơn nhiều dù total time như nhau.

ToolDispatcher: Security boundary

ToolDispatcher là component dễ bị underestimate nhất. Nhìn qua thì chỉ là routing — LLM muốn gọi tool nào thì route đến đó. Nhưng dispatcher là security boundary duy nhất giữa LLM decision và actual execution.

 ToolDispatcher.execute(tool_use):

 1. Validate tool exists trong registry
    Nếu không → ToolNotFoundError (LLM có thể hallucinate tên tool)

 2. Validate input schema
    Nếu không match → ValidationError với expected schema
    (Trả về LLM để nó biết sửa input)

 3. Check permissions
    Tool có trong allowed list không?
    Input path có nằm trong allowed directories không?
    (Security gate)

 4. Nếu tool là destructive → Request confirmation từ CLI Layer
    User confirm? → proceed
    User deny?    → return cancellation result

 5. Execute tool

 6. Wrap result:
    {
      type: "tool_result",
      tool_use_id: tool_use.id,
      content: result
    }

Tại sao validation ở đây? Vì LLM không phải luôn trả về valid input. Đặc biệt khi tool definitions phức tạp, LLM có thể misunderstand parameter names hoặc formats. Validation ở Dispatcher cho phép return clear error về LLM để nó retry với correct input.

Tại sao permission check ở đây? Đây là single point of truth. Nếu permission check nằm trong từng tool implementation, một tool mới bị quên implement permission check sẽ tạo ra security hole. Centralize ở Dispatcher đảm bảo không có bypass.

Guardrails: Loop không chạy mãi

Agent Core manage guardrails — không phải CLI Layer, không phải tools:

 Loop state:

 ┌─────────────────────────────────────────────────────┐
 │  iteration_count:    0  (max: 20)                  │
 │  total_tokens:       0  (max: 100,000)             │
 │  start_time:         now (timeout: 5 minutes)      │
 │  consecutive_errors: 0  (max: 3)                   │
 └─────────────────────────────────────────────────────┘

 Trước mỗi vòng loop:
   if iteration_count >= max     → stop, báo cáo
   if total_tokens >= max        → stop, báo cáo
   if elapsed > timeout          → stop, báo cáo
   if consecutive_errors >= max  → stop, báo cáo

Guardrails không phải pessimism — chúng là engineering cho failure case. Mọi production system cần chúng.

Trade-off: Fat Core vs Thin Core

 Fat Agent Core:
   Business logic trong Core
   Tool-specific handling trong Core
   Formatting trong Core
   → Easier để start
   → Harder để test (phụ thuộc vào LLM)
   → Harder để change (thay đổi một thứ ảnh hưởng nhiều thứ)

 Thin Agent Core:
   Chỉ orchestration
   Business logic trong Tools
   Formatting trong CLI Layer
   → Harder để start (cần discipline)
   → Easier để test từng phần
   → Easier để change và extend

Luôn chọn Thin Core. Khi gặp temptation để add business logic vào Core, đó là signal để tạo tool mới hoặc move logic xuống Tool Layer.

Kết luận

  • Agent Core có 4 component: ContextBuilder, LLMClient, ToolDispatcher, Guardrails.
  • ContextBuilder assembly messages[] theo priority order — system prompt và current message không bao giờ bị trim.
  • LLMClient là thin HTTP wrapper với SSE streaming — không có business logic.
  • ToolDispatcher là security boundary: validate, authorize, execute, wrap result.
  • Guardrails prevent infinite loops và runaway cost — không phải optional.
  • Thin Core là đúng — business logic sống trong Tools, không trong Core.