Series 1 - Bài 3: CLI Layer từ keystroke đến LLM request

CLI Layer không chỉ là I/O đơn giản — nó định nghĩa UX của agent. Bài này phân tích pipeline từ keystroke đến khi request được gửi đến Agent Core, bao gồm input handling, command parsing và output streaming.

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

CLI Layer thường bị xem nhẹ — "chỉ là I/O, không có gì phức tạp". Nhưng đây là layer người dùng tương tác trực tiếp, và design của nó ảnh hưởng đến toàn bộ trải nghiệm làm việc với agent.

Một CLI Layer được thiết kế tốt là invisible — người dùng tập trung vào task, không phải vào cách gõ lệnh hay đọc output.

Pipeline tổng thể

 Keystroke (user)
      │
      ▼
┌─────────────────┐
│  InputHandler   │
│  - readline wrap│
│  - multiline    │
│  - history nav  │
└────────┬────────┘
         │ raw input string
         ▼
┌─────────────────┐
│  CommandParser  │
│  /command?      │
└────────┬────────┘
         │
    ┌────┴────┐
    │         │
 /cmd      message
    │         │
    ▼         ▼
Handle    Send to
locally   AgentCore
            │
            │ (AgentCore processes...)
            │
            ▼
┌─────────────────┐
│ OutputRenderer  │
│ - stream chunks │
│ - tool status   │
│ - ANSI format   │
└────────┬────────┘
         │
         ▼
     stdout

InputHandler: Hơn là readline wrapper

Node.js built-in readline cung cấp cơ bản. InputHandler build thêm:

Multiline Input

User thường muốn paste đoạn code dài hoặc mô tả chi tiết. Terminal mặc định gửi ngay khi nhấn Enter. InputHandler cần detect khi nào đây là multiline input.

Detection strategies:

1. Bracket mode: user gõ ``` để bắt đầu, ``` để kết thúc
   → Phổ biến nhất, dễ implement

2. Trailing backslash: dòng kết thúc bằng \
   → Familiar với shell users

3. Paste detection: burst of input trong <10ms
   → Cần timing detection, phức tạp hơn

Command History

User muốn dùng phím mũi tên để navigate qua previous commands. readline có built-in history nhưng chỉ cho session hiện tại. Nếu muốn persist qua sessions, cần lưu vào file.

History management:

 In-memory (session only):
   readline.history.push(input)
   → Đơn giản, mất khi thoát

 Persistent:
   Đọc ~/.agent_history khi start
   Ghi sau mỗi command
   Giới hạn size (last 1000 entries)
   → Phức tạp hơn, tiện lợi hơn

Signal Handling

User cần có cách interrupt agent đang chạy mà không kill process:

Ctrl+C trong khi agent thinking  → Interrupt current task
Ctrl+C trong khi agent chờ input → Exit
Ctrl+D → Graceful shutdown

CommandParser: Phân loại input

Không phải mọi input đều là message cho agent. CommandParser phân biệt:

Input starts with '/'?  →  Command
Otherwise              →  Agent message

Built-in Commands

/help     → Liệt kê available commands và tools
/clear    → Xóa conversation history, giữ system prompt
/context  → Hiển thị current context (files đang load, token count)
/tools    → Liệt kê available tools và descriptions
/exit     → Graceful shutdown
/model    → Hiển thị model đang dùng, có thể switch

Commands được handle hoàn toàn trong CLI Layer — Agent Core không biết về chúng.

Edge Cases

Empty input        → Bỏ qua, không gửi đến agent
Whitespace only    → Bỏ qua
Unknown command    → Hiển thị lỗi + suggest /help
Message quá dài    → Warning về token limit trước khi gửi

OutputRenderer: Streaming và formatting

Output rendering là nơi CLI trở nên có personality.

Streaming Text

Agent response được stream word by word từ LLM. Renderer nhận chunks và in trực tiếp:

Stream chunks:
  "The" → print
  " file" → print
  " has" → print
  " been" → print
  " refactored." → print
  [DONE] → newline

Không buffer toàn bộ response rồi mới print — đó là anti-pattern vì user phải đợi.

Tool Status Indicators

Khi agent đang execute tool, user cần biết gì đó đang xảy ra:

Agent đang think:     ● thinking...
Agent gọi tool:       ⚡ read_file: src/payment.service.ts
Tool thực thi xong:   ✓ read_file (234 lines)
Agent gọi tool khác:  ⚡ run_command: npm test
Command chạy xong:    ✓ run_command (2.3s, exit 0)

Status indicators giúp user hiểu agent đang ở đâu trong process, không cảm thấy như app bị treo.

ANSI Formatting

Text thường:    white
Tool names:     cyan bold
File paths:     yellow
Error messages: red
Success:        green
Code blocks:    dim background

Nhưng cần detect khi nào terminal không support ANSI — ví dụ khi output được pipe vào file:

Nếu stdout không phải TTY → disable ANSI codes
Nếu NO_COLOR env var set  → disable ANSI codes

Syntax Highlighting cho Code

Agent thường return code blocks. Basic highlighting:

Detect language từ markdown fence (```typescript)
Apply keyword highlighting
Line numbers optional

Library highlight.js có thể dùng, hoặc implement basic regex-based highlighting cho performance.

Trade-off: Rich CLI vs Simple CLI

            Richer CLI
               │
    Better UX  │  Thêm dependencies
    More       │  Thêm code để maintain
    informative│  Khó test terminal rendering
               │
            Simple CLI
               │
    Easy to    │  Bare-bone experience
    implement  │  User cần đọc raw output
    Easy to    │
    test       │

Với internal developer tool, lean toward simple. Với tool distribute cho người khác, invest vào UX.

Kết luận

  • CLI Layer pipeline: keystroke → InputHandler → CommandParser → (AgentCore) → OutputRenderer → stdout.
  • InputHandler cần xử lý multiline, history, signal handling — không chỉ là readline wrapper.
  • CommandParser tách meta-commands khỏi agent messages — CLI Layer handle commands, không pass xuống.
  • OutputRenderer định nghĩa agent personality: streaming, tool status, formatting.
  • Boundary cứng: CLI Layer không biết về LLM hay tools — chỉ biết terminal và user interaction.