my-agent-crew

Kiến trúc hệ thống: giải phẫu một agent harness

Phiên bản: 0.20.0 · Cập nhật: 2026-10-14

Tài liệu này dành cho người chưa từng xây agent harness. Nó trả lời ba câu hỏi: harness gồm những gì, mỗi phần làm việc gì, và chúng khớp với nhau ra sao khi một tin nhắn đi qua. Mọi ví dụ lấy từ một bộ cài thật của my-agent-crew: một master “Trợ lý”, ba agent việc cá nhân (Pong, HLV sức khoẻ, sổ cái) và ba agent kỹ thuật (lập trình, cố vấn, nghiên cứu), tất cả chạy trong một tiến trình trên máy cá nhân, nói chuyện qua web UI và một bot Telegram.

Năm sơ đồ trong tài liệu là bản động: nhúng thẳng vào trang, có animation và thanh chuyển view — bấm tên view để xem từng lớp. Nếu trang được đọc ở nơi không chạy được HTML nhúng (GitHub, trình đọc markdown), dùng link “Mở riêng” hoặc ảnh tĩnh SVG ngay dưới mỗi sơ đồ. Gallery cả năm: diagrams/index.html; cách publish ở deployment-guide.md §9.

1. Harness là gì

Model ngôn ngữ chỉ làm một việc: nhận một danh sách message, trả về một message. Nó không nhớ gì giữa hai lần gọi, không chạm được vào đĩa, không gọi được API, không biết hôm nay là ngày nào. Mọi thứ còn lại — nhớ, làm, hỏi lại, dừng đúng lúc — là việc của phần mềm bao quanh model. Phần mềm đó gọi là harness.

Một harness tối thiểu có bốn việc:

Việc Câu hỏi nó trả lời Ở my-agent-crew
Lắp ngữ cảnh Model cần biết gì trước khi đọc tin nhắn? system prompt lắp lại từ tệp trong home mỗi vòng
Vòng lặp tool Model muốn làm gì, ai làm, kết quả về đâu? một vòng lặp lượt + một sổ đăng ký tool
Lưu và tiếp tục Lượt sau biết gì về lượt trước? SQLite + MEMORY.md + ghi chú ngày
Kiểm soát Khi nào dừng, khi nào hỏi người? approval, cost_cap_usd, max_steps

my-agent-crew thêm hai tầng lên trên: nhiều agent (mỗi agent là một thư mục tệp) và nhiều kênh (web, Telegram, lịch cron), tất cả đổ vào cùng một vòng lặp.

2. Giải phẫu harness

Bốn view: Một cổng vào, Vòng lặp agent, Ngữ cảnh từ đĩa, Quan sát và lưu. Mở riêng · ảnh tĩnh.

Sơ đồ chia làm ba vùng. Đọc từ trái sang phải: người dùng đi vào qua kênh giao tiếp, mọi việc xảy ra trong runtime (một tiến trình Python), và mọi thứ tồn tại lâu dài nằm trong home trên đĩa.

2.1 Kênh giao tiếp

2.2 Inbound: một cổng vào

Inbound là cổng duy nhất cho tin nhắn từ người. Nó làm ba việc nhỏ nhưng quan trọng:

  1. Tìm hoặc tạo cuộc trò chuyện theo kênh và ngày (một cuộc trò chuyện mỗi ngày mỗi kênh cho master).
  2. Giữ mỗi cuộc trò chuyện một lượt một lúc. Tin đến khi lượt trước còn chạy không chạy song song, cũng không bị từ chối: nó vào hàng của cuộc trò chuyện. Tin thường chờ lượt đó xong rồi cùng mọi tin chờ với nó được trả lời bằng một lượt riêng, chạy nền. Tin bắt đầu bằng /steer <chữ> hay một lệnh trong kit của agent được chèn vào chính lượt đang chạy, ở ranh giới tool kế tiếp. Chỉ cuộc trò chuyện đang chờ duyệt mới từ chối tin mới (HTTP 409).
  3. Chạy lượt và đăng ký run với activity hub.

Job lịch không đi qua Inbound: scheduler chạy thẳng một lượt với nguồn job trên cuộc trò chuyện riêng của job. Hai đường, một vòng lặp.

2.3 Vòng lặp agent

Gói agent/ là trái tim. Một lượt:

messages = lịch sử cuộc trò chuyện + tin mới
lặp tối đa max_steps:
    messages += tin /steer đang chờ              # chèn ở ranh giới tool
    system = lắp system prompt từ tệp của agent   # lắp lại mỗi vòng
    reply  = model(system, messages, mô tả tool)
    nếu reply không gọi tool: kết thúc, lưu, trả lời
    với mỗi tool_call:
        nếu tool cần duyệt và cuộc trò chuyện không autonomous:
            tạo approval, phát ApprovalRequired, dừng lượt (tiếp tục sau khi duyệt)
        result = chạy tool                          # cắt còn tool_output_chars
    messages += reply + results

Hết bước. Lần gọi model cuối cùng max_steps cho phép mà đã trả lời thì lượt xong như mọi lượt khác. Nếu nó còn xin gọi tool, các lệnh đó không chạy mà nhận kết quả nói lượt đã hết bước, rồi lượt dừng (halted, lý do max_steps). Nhờ vậy tin kế tiếp không phải đóng chúng thành lệnh bị ngắt mà model không biết đã chạy hay chưa.

Chặn lặp. Model rẻ kẹt ở một lệnh hỏng có thể gọi lại y hệt nó tới hết max_steps: dài, tốn tiền và không đi tới đâu. Mỗi lượt có một bộ chặn (agent/loop_guard.py) đọc các tool call của một lần gọi model thành một chữ ký (tên cộng tham số, không tính id, không tính thứ tự song song) và đếm chữ ký giống hệt liên tiếp. Lần thứ ba, model nhận một tin nhắc chỉ nêu tên công cụ và số lần — không chép tham số hay kết quả, vì tin này nằm ở vai người dùng mà dữ liệu từ web hay tệp thì không bao giờ được thành lời của người. Gọi y hệt thêm ba lần nữa thì lượt dừng (halted, lý do loop); lệnh cuối không chạy mà nhận kết quả từ chối, để cuộc trò chuyện vẫn tiếp được. Chỉ lặp liền nhau mới tính: “chạy test, sửa, chạy lại” đổi gì đó ở giữa, và đó là cách làm việc. progress_note và ask_user không tính; một lần gọi model không có tool, hay tin mới của người, đếm lại từ đầu.

Leo thang. Agent nào nêu escalation_route trong agent.yaml thì hai kiểu kẹt không dừng lượt ngay mà chuyển lượt sang tuyến đó (agent/escalation.py); mặc định không agent nào nêu. Khi bộ chặn lặp sắp dừng lượt: các lệnh lặp vẫn không chạy, nhưng kết quả từ chối nói rằng lượt được chuyển sang model khác, bộ đếm lặp về không và lần gọi model kế tiếp đi trên tuyến leo thang. Khi một lần gọi model hỏng trên mọi tuyến trong routes mà chưa hiện chữ nào (một mẩu bản nháp canvas cũng tính là đã hiện): chính yêu cầu đó được hỏi lại trên tuyến leo thang ở bước kế tiếp, và lần gọi hỏng vẫn tính một bước của max_steps. Lượt chỉ chuyển một lần và ở lại tuyến đó tới hết; lặp tiếp hay lỗi tiếp ở đó thì lượt dừng đúng như agent không có tuyến leo thang. Không chuyển khi lượt không còn lần gọi model nào, khi hết bước, hay khi hết ngân sách: model mạnh hơn không chữa được ba việc đó. Lượt sau bắt đầu lại trên routes, và phần lượt đi tiếp sau khi chờ duyệt hay sau khi server khởi động lại cũng vậy. Việc chuyển phát event escalated (lý do loop hoặc error, tuyến, và lỗi nếu có): web hiện một dòng báo phía trên ô nhập, còn run có step escalation, đứng vào chỗ step model đang chờ khi lý do là lỗi.

Điểm cần nhớ với người mới: model không “chạy” gì cả. Nó chỉ trả về JSON nói “tôi muốn gọi workspace_read với path này”. Harness quyết định có chạy không, chạy rồi đưa kết quả vào message tiếp theo. Mọi cổng kiểm soát nằm ở chỗ này.

2.4 Sổ đăng ký tool: tay chân của agent

Gói tools/ giữ danh sách tool mà agent được dùng (tools: trong agent.yaml). Mỗi tool có phần mô tả (tên, mô tả, tham số JSON — đưa cho model) và phần chạy (hàm Python thật). Bộ tool có sẵn:

Nhóm Tool Cần duyệt
Workspace workspace_list, workspace_read, workspace_glob, workspace_grep không
Workspace workspace_write, workspace_edit có
Shell shell_run (cwd = workspace, có shell_ask_patterns và shell_deny_patterns; shell_network: false hoặc shell_write_paths thì chạy trong sandbox-exec, chỉ ghi dưới các đường dẫn đó, không mạng nếu shell_network: false) có
Lịch schedule_create (đề xuất một job prompt lặp lại) luôn luôn, không cấu hình nào tắt được
Web fetch_url, web_search không
Trình duyệt browser_snapshot, browser_text, browser_tabs, browser_screenshot, browser_pdf (chỉ agent có khoá browser) không
Trình duyệt browser_navigate, browser_act, browser_download (browser_act và browser_download chỉ khi browser: act) có; host trong browser_hosts thì browser_navigate không hỏi, còn host ngoài danh sách, tên phần tử mang từ như “thanh toán”, hay một lần tải tệp lên thì hỏi cả khi autonomous
Desktop computer_state, computer_screenshot, computer_observe (chỉ agent có computer: "on") không
Desktop computer_act (cùng khoá) có; đưa ứng dụng lên trước, bấm theo toạ độ, Enter hay Space một mình, phím ngoài nhóm thường dùng, hay chữ và tên phần tử mang từ dừng thì hỏi cả khi autonomous
Trí nhớ memory_save, memory_search, user_memory_save, user_memory_forget, wiki_get, wiki_search, wiki_apply không
Đội delegate không
Canvas artifact_create, artifact_read, artifact_edit, artifact_rewrite, artifact_list, artifact_import không (bản cũ nào cũng khôi phục được)
Canvas artifact_export (ghi đè một tệp trong workspace) có
Đầu ra dài tool_output_read (đọc lại bản gốc của kết quả đã bị rút ngắn, chỉ trong cuộc trò chuyện hiện tại) không
Với người ask_user (dừng lượt, chờ câu trả lời), progress_note (một câu “đang làm gì” lên timeline) không
Khác image_read, pdf_read, skill_read không

Ngoài bộ có sẵn, agent nhận thêm tool của những máy chủ MCP mà agent.yaml của nó nêu trong mcp:. Máy chủ được khai một lần trong config.yaml (mcp_servers), gói mcp/ nói chuyện với chúng qua streamable HTTP, và mỗi tool vào sổ đăng ký dưới tên mcp__<máy chủ>__<tool>. Chúng đi qua cùng cổng duyệt: mọi tool MCP hỏi trước khi chạy, trừ những tool chủ ghi là chỉ đọc. Mặc định chúng không được khai sẵn cho model: agent tìm bằng tool_search, tool tìm thấy được khai từ lời gọi model kế tiếp, và cái đã nạp được đọc lại từ chính hội thoại nên còn nguyên sau restart.

Tám tool browser_* cũng không theo tools hay mode: chúng đến theo khoá browser trong tệp của agent, trên máy đã bật browser_enabled và đã cài trình duyệt. Gói browser/ giữ cho mỗi agent một Chromium với hồ sơ riêng ở <home>/browser/<agent id>/, mở ở lời gọi đầu tiên; tab thuộc về cuộc trò chuyện đã mở nó. Model không nhận HTML mà nhận một dàn ý của trang, mỗi phần tử một ref, đặt giữa hai dòng rào mang mã ngẫu nhiên để chữ trên trang là dữ liệu chứ không phải chỉ dẫn. Hai hàng rào nằm trong mã và không có công tắc: mọi request của trang (kể cả chuyển hướng, tài nguyên con, WebSocket) đi qua một proxy trong chính tiến trình server, nơi địa chỉ không công khai bị từ chối; và thư mục hồ sơ đóng với mọi tool tệp cùng mọi lệnh shell trong sandbox. browser_act chỉ làm khi phần tử còn đúng là thứ model vừa được cho thấy: cùng trang, cùng ref, cùng vai trò, cùng tên; browser_download bấm theo đúng phép ấy rồi giữ tệp trang gửi về, còn browser_pdf in trang đang hiện, và cả hai lưu vào downloads/ của workspace. Agent không gõ được vào ô mật khẩu, nên chủ đăng nhập hộ nó trong một cửa sổ có giao diện mở trên màn hình máy chạy đội, dùng chính hồ sơ ấy. Một hồ sơ chỉ thuộc một trình duyệt mỗi lúc: trình duyệt của agent dừng trước khi cửa sổ mở, và trong lúc cửa sổ mở mọi tool trình duyệt của agent ấy từ chối và nói lý do. Chi tiết ở tools.md.

Bốn tool computer_* đi theo cùng lối: khoá computer trong tệp của agent, không theo tools hay mode, và máy không có công tắc chung. Khác với trình duyệt, desktop là chính màn hình chủ máy đang dùng, và server không tự chụp hay bấm gì. Người làm là trình trợ giúp desktop (gói Swift desktop-helper/): một tiến trình riêng, một ứng dụng macOS không biểu tượng chạy trong phiên đăng nhập của chủ, nạp bằng một LaunchAgent, và là thứ duy nhất giữ hai quyền Ghi màn hình và Trợ năng của hệ điều hành. Gói desktop/ của server nói chuyện với nó qua một Unix socket (mặc định <home>/desktop/sock), mỗi yêu cầu một dòng JSON trên một kết nối riêng; server chỉ nói chuyện với socket thuộc chính người dùng chạy nó, trong một thư mục không ai khác ghi được. Không có trình trợ giúp thì bốn tool vẫn ở đó và nói vậy. Model nhận ảnh chụp kèm một frame_id, hay dàn ý phần tử của một ứng dụng với mỗi phần tử một ref, đặt trong rào như trang web; computer_act chỉ chạy trên frame_id còn là khung hình mới nhất và trên ref của dàn ý hội thoại vừa được xem, chỉ trên ứng dụng trong computer_apps khi nó đang ở trước. Hàng rào nằm ở cả hai phía: server từ chối trước khi ai bị hỏi (ứng dụng ngoài danh sách, chữ như sudo, tổ hợp phím đóng ứng dụng hay đổ thùng rác, chữ cho ô bảo mật), còn trình trợ giúp tự xét lại ứng dụng ở trước, khung hình, màn hình khoá và chế độ nhập bảo mật ngay trước khi thao tác. Thư mục <home>/desktop/ đóng với mọi tool tệp và mọi lệnh shell trong sandbox; shell không sandbox thì với tới socket như mọi tiến trình khác của chủ máy, nên agent bật desktop nên có shell đã sandbox. Chi tiết ở tools.md, cài đặt ở deployment-guide.md.

Agent giữ ít nhất một tool script gọi được thì có thêm tool_script: một tool có sẵn chỉ đọc, hay một tool MCP chủ ghi là chỉ đọc (read_only, không hidden). Nó không cần máy chủ MCP nào và không theo tools hay mode: server/agent_assembly.py giao nó lúc dựng agent, và mỗi lần giao tool MCP (mcp/handout.py) nó được giao lại với lời mô tả mới, đứng sau tool có sẵn, delegate và tool_search. Gói script/ chạy đoạn script của model trong một tiến trình con, script gọi tool qua chính server, và chỉ phần nó in ra mới quay về model; từng lời gọi được ghi vào step của run. Mặc định script chỉ gọi tool chỉ đọc. Agent khai script_calls: gated thì script gọi được cả tool đổi trạng thái: script/gate.py đưa từng lời gọi như thế qua đúng phán quyết của cổng duyệt (agent/tool_gate.py) trước khi lời gọi nào của lô chạy. Lời gọi lẽ ra phải hỏi người, hay bị chủ cấm, kết thúc script bằng một dòng bảo model gọi trực tiếp, vì script không bao giờ chờ người. Ở chế độ đó script ghi được canvas như một lời gọi trực tiếp, và script của agent giữ delegate giao được việc cho agent khác bằng tools.delegate: cuộc trò chuyện con treo dưới lượt đã giao, và số agent kia tiêu đi theo lời gọi chứ không cộng vào run. Script giữ giá trị JSON giữa các lần chạy bằng store và load (bảng script_store, theo agent và theo job hay cuộc trò chuyện); các lần ghi chỉ được áp khi script chạy xong. Chưa rõ tên hay tham số của một tool thì script tra bằng tools.names, tools.search và tools.describe: những lời tra đó chỉ hỏi tiến trình giữ tool, không chạy tool nào và không phải một lời gọi. Nhiều lời gọi không cần nhau thì script gộp một lô bằng tools.batch: server chạy chúng cùng lúc (tối đa bốn, một lô tối đa mười lời gọi), vẫn qua hook của agent từng cái một, trả lời và ghi lên thẻ run theo đúng thứ tự đã viết; mỗi lời gọi của lô vẫn là một trong 25 lần, và lô có một lời gọi script không được gọi thì không lời gọi nào chạy. Việc code không quyết nổi thì script hỏi model bằng llm.classify và llm.summarise: tiến trình con chỉ gửi câu hỏi, còn server (script/model_calls.py) gọi model của chính agent trên route đầu tiên, không khai tool nào, đọc câu trả lời thành một nhãn script đã đưa hay một bản tóm tắt đã cắt (script/model_answers.py) rồi trả về đúng chuỗi đó. Mỗi lần hỏi là một lời gọi của script: tính vào 25 lần, vào trần của lượt, lên thẻ run kèm giá, và vào sổ cái sử dụng dưới mục đích script.

Một script có thể chạy vài phút, nên lời gọi của nó không đợi kết quả của script mới tới người xem. Lượt chạy một lời gọi tool lẻ, và tool_script luôn là lời gọi lẻ, trong một task riêng (agent/tool_run.py) và trao cho task đó một chỗ để nói (một ContextVar trong agent/nested_calls.py); script nói qua chỗ đó mỗi khi một lời gọi của nó xong, và lượt phát sự kiện nested_call xen giữa tool_call và tool_result. Lượt bị dừng thì task bị huỷ và được chờ cho xong, nên tiến trình con của script không sống lâu hơn lượt. Run liệt kê lời gọi và cộng tiền ngay (activity/tool_steps.py), web thêm một dòng vào thẻ run và thẻ tool. Kết quả của script nêu lại toàn bộ theo thứ tự đã viết cùng hai số đo, received_chars (số ký tự các lời gọi trả cho script, kể cả chuỗi nhận về mỗi lần hỏi model) và printed_chars (số ký tự script in ra), và chỉ lời gọi chưa được báo mới được cộng tiền. Cả ba được lưu kèm tin nhắn tool của script trong trường context, thứ model không bao giờ được gửi, để thẻ trong khung chat còn liệt kê sau khi tải lại trang.

Ba rào giữ script trong khuôn khổ của lượt. Mọi script của một lượt dùng chung một trần lời gọi, mười cho mỗi lần gọi model lượt được phép (max_steps × 10): script vượt trần bị dừng tại chỗ, các lời gọi sau nó được đóng lại không chạy và lượt dừng với lý do nested_calls. Câu trả lời của một tool dài hơn 200.000 ký tự thì script nhận phần đầu, bản đầy đủ nằm trong kho kết quả dài (tools/output_spill.py) dưới id <id lời gọi script>.<số>, và script đọc tiếp bằng tool_output_read. Script in ra quá 60.000 ký tự thì model nhận phần đầu và phần cuối (script/printed.py), script không bị dừng.

Chi tiết từng tool ở tools.md.

2.5 Provider: model là dịch vụ ngoài

Gói llm/ nói chuyện với OpenRouter (và Ollama) và bọc thành một chuỗi tuyến: một danh sách model theo thứ tự (routes: trong agent.yaml), model đầu lỗi hoặc quá tải thì thử model tiếp và phát RouteFallback để người dùng thấy. Provider giả (MY_AGENT_ROUTES=fake:echo) để chạy test và thử harness không tốn tiền.

2.6 Scheduler

scheduler/ đọc schedules: của mọi agent, mỗi phút kiểm tra cron, đến hạn thì tạo một job run: một cuộc trò chuyện autonomous với prompt (hoặc lệnh kit) định sẵn, hoặc một script chạy mà không mở lượt nào của agent, trừ khi nó in ra gì đó và lịch có then_prompt (xem agents.md). Trạng thái job lưu ở bảng job_state, xem ở /api/jobs. Ngoài agent.yaml, một job có thể tới từ bảng created_schedules — agent tự đề xuất qua tool schedule_create, người duyệt nguyên văn prompt, rồi job chạy ngay không cần khởi động lại (origin: "chat" trên job, đối lập origin: "profile"); xem design.md và tools.md.

Cùng vòng đời với scheduler, server/housekeeping.py chạy một vòng quét mỗi ngày (ngay khi khởi động, rồi mỗi 24 giờ, ngoài event loop) xoá tệp trong spill/ cũ hơn 7 ngày; một lần quét lỗi chỉ được ghi log, vòng vẫn chạy tiếp. Xem tools.md.

2.7 Home: agent là tệp

Mọi thứ làm nên một agent là tệp văn bản trong ~/.my-agent-crew/agents/<id>/:

Tệp Vai trò Khi nào được đọc
agent.yaml model, tool, giới hạn, lịch, delegates, Telegram khi nạp roster
AGENTS.md vai trò và cách làm việc mỗi lượt, đầu system prompt
SOUL.md giọng và tính cách mỗi lượt
MEMORY.md điều agent tự đúc kết mỗi lượt
memory/YYYY-MM-DD.md ghi chú ngày hôm qua + hôm nay, ngay trước tin mở lượt (chỉ phần mới); 7 ngày khi consolidate
skills/*.md kỹ năng chỉ mục mỗi lượt, nội dung khi skill_read
.agents/ kit lệnh/agent/hook kiểu Claude Code lệnh vào roster; hook chạy quanh tool
workspace/ nơi tool đọc/ghi khi tool chạy

Dùng chung cho mọi agent: config.yaml, agent.sqlite3, users/owner/ (USER.md + facts), skills/ và .agents/ cấp home, cùng browser/<id>/, hồ sơ trình duyệt của từng agent (không tool tệp nào đọc được), và desktop/, nơi trình trợ giúp desktop mở socket của nó (cũng đóng với mọi tool tệp). Bố cục đầy đủ ở agents.md.

2.8 Canvas: tài liệu người và agent cùng sửa

Canvas là tài liệu có phiên bản nằm cạnh cuộc trò chuyện; vì sao nó có hình dạng này ở design.md, cách dùng ở canvas.md.

Kho. Canvas sống trong cùng agent.sqlite3, ở bốn bảng: artifacts (mỗi canvas một dòng), artifact_versions (mọi phiên bản từng ghi), conversation_artifacts (canvas nào thuộc cuộc trò chuyện nào) và canvas_focus (cuộc nào đang mở canvas nào, đang chọn đoạn nào). Ba cột đi kèm: messages.context giữ ghi chú canvas của một tin (mục 6), còn conversations.root_id và conversations.root_source ghi cuộc trò chuyện gốc của một chuỗi giao việc và kênh của nó, để agent được giao việc biết mình có được ghi canvas hay không. Bảng và cột được thêm tự động lúc khởi động.

Bề mặt. Web làm mọi việc với canvas qua /api/artifacts (liệt kê, tạo, đọc, lưu, đổi tên, xoá, lịch sử, khôi phục, tải về, nhập lại từ tệp nguồn) và báo canvas đang mở của một cuộc qua /api/conversations/{id}/canvas; mọi lần ghi qua REST là của người, còn agent ghi bằng tool. Bảng đầy đủ ở canvas.md.

Trang chạy cách ly. GET /api/artifacts/{id}/render chỉ có cho canvas html và mermaid. Trang trả về chạy trong một origin mờ: không với tới ứng dụng và không gửi được gì ra ngoài, chỉ tải script, style và font từ vài CDN công khai. Các loại khác không có đường này; svg và ảnh hiện như hình. Xem canvas.md.

Event. Luồng SSE của một lượt có thêm hai event chỉ để hiển thị, không lưu và không lên luồng hoạt động: user_context đưa ghi chú canvas của tin vừa gửi về tab đã gửi nó, và tool_call_delta đưa từng phần đối số của một lời gọi tool khi model còn đang viết, để web hiện canvas đang được viết. Luồng hoạt động có thêm event artifact mỗi lần một canvas được tạo, ghi, đổi tên, khôi phục hay xoá, nên mọi tab đang mở cập nhật theo.

3. Một lượt chat: từ Telegram đến câu trả lời

Bốn view: Nhận tin, Lượt của master, Giao việc cho HLV, Trả lời. Mở riêng · ảnh tĩnh.

Ví dụ thật: người dùng nhắn bot “hôm nay ăn thế nào cho hợp lịch tập?”.

  1. Poller Telegram nhận update, chuyển thành POST /api/inbound {text}. Ảnh (nếu có) được tải về workspace/inbox/ và đưa vào tin nhắn dưới dạng đường dẫn.
  2. Inbound tìm cuộc trò chuyện telegram:<chat> của hôm nay, thấy rảnh, chạy một lượt cho master.
  3. Master lắp system prompt: persona của “Trợ lý”, USER.md + facts, MEMORY.md, tóm tắt cuộc hôm qua, roster (sáu agent, mỗi agent một dòng mô tả và các dòng hệ thống tự đọc từ đội đang chạy: máy chủ MCP, kỹ năng, lịch, công cụ, model), lệnh kit, ghi chú ngày. Model thấy trong roster có “HLV sức khoẻ” và gọi tool delegate(agent="health-coach", task=…).
  4. Delegate chạy một lượt con: agent con có persona riêng, tool riêng, workspace riêng, và chỉ nhận brief — không thấy lịch sử chat của master. Lượt con là cuộc trò chuyện autonomous nên tool cần duyệt không dừng.
  5. Kết quả lượt con quay về master dưới dạng tool result. Master viết câu trả lời cuối, harness lưu message, phát Done, poller gửi về Telegram.

Trên web UI, toàn bộ chuỗi này hiện thành một run với các step lồng nhau, kể cả run con của HLV.

4. Master giao việc cho đội

Ba view: Từ yêu cầu đến trả lời, Ai làm gì, Cổng và giới hạn. Mở riêng · ảnh tĩnh.

Trong bộ cài thật, master “Trợ lý” để trống delegates:, nghĩa là giao được cho cả sáu agent còn lại:

Agent Vai trò Nguồn
pong thư ký cá nhân: bản tin sáng, mail, lịch, tasks, Goodreads viết tay, chuyển từ openclaw
ledger sổ cái tài chính, nhắc hạn, giá vàng; không tool mạng, không ghi trí nhớ dùng chung viết tay, tách khỏi pong
health-coach HLV sức khoẻ: ăn, tập, Garmin, sách viết tay, chuyển từ openclaw
fullstack-developer, kongming, researcher làm phần mềm trọn gói · cố vấn chỉ đọc · tra cứu mọi chủ đề agent add <template>

Cách quyết định giao hay tự làm nằm ở model của master, nhưng harness bảo đảm ba điều:

Web UI ghim kết quả delegate vào thread của master để người đọc thấy ai đã trả lời gì.

5. Cổng duyệt tool

Ba view: Cổng duyệt, Đường tắt, Kết thúc. Mở riêng · ảnh tĩnh.

Đây là cơ chế khiến harness khác một script gọi API. Khi model gọi shell_run, workspace_write hoặc workspace_edit:

  1. Harness tạo một dòng trong bảng approvals, phát ApprovalRequired, và dừng lượt. Không có gì chạy.
  2. Người dùng thấy thanh duyệt trên web UI. POST /api/approvals/{id} với approve, deny, hoặc approve + always.
  3. Duyệt: harness chạy tool, kết quả về model, lượt tiếp tục như chưa từng dừng. always ghi tên tool vào auto_approve của cuộc trò chuyện đó.
  4. Từ chối: model nhận tool result DENIED_TOOL và tự quyết bước tiếp — lượt không hỏng.
  5. Hết hạn: scheduler quét định kỳ; approval quá hạn bị đánh dấu expired và xử lý như từ chối. Hạn là approval_ttl_seconds của cuộc trò chuyện — lịch hay khối telegram đã mở nó đặt, cuộc của agent con chép của cha — không có thì của config.yaml (600 s). Fail-closed: không ai trả lời thì không chạy, và kết quả tool nói chưa có gì thay đổi, không làm lại hay làm việc tương đương bằng tool khác.

Hai đường tắt có chủ đích:

Cả hai chiều ấy là của từng lời gọi chứ không của tool: một tool khai được hàm ask_when (lời gọi này phải hỏi, kèm lý do hiện trên thẻ duyệt) và hàm allow_when (lời gọi này là việc thường, chạy cả trong cuộc có giám sát). Cổng (agent/tool_gate.py) xét theo thứ tự cố định: ask_user luôn hỏi; lời gọi có lý do phải hỏi thì hỏi dù autonomous; lời gọi được coi là việc thường thì chạy; còn lại theo autonomous và auto_approve của cuộc trò chuyện. Shell đi đúng thứ tự ấy với shell_ask_patterns và shell_allow_patterns; hai tool trình duyệt khai hai hàm đó để xét host của trang (browser_hosts) và tên của phần tử sắp bấm (browser_ask_patterns). computer_act khai ask_when cho những thao tác luôn hỏi, còn allow_when của nó chỉ nhận lời gọi chắc chắn bị từ chối, để lời từ chối tới thẳng lượt mà không ai bị hỏi.

Khi một approval đang chờ, /api/inbound trả 409 để người dùng không vô tình mở lượt thứ hai trên cùng cuộc trò chuyện. Quyết định giữ cuộc trò chuyện ngay lúc được đưa ra, trước cả khi lượt tiếp tục kịp chạy: tin gửi ngay sau đó vào hàng chờ lượt đó, còn quyết định thứ hai cho cùng yêu cầu nhận 409.

6. Ngữ cảnh đi vào, trí nhớ đi ra

Bốn view: Vào system prompt, Theo yêu cầu, Ra đĩa, Cô đọng. Mở riêng · ảnh tĩnh.

Quy tắc duy nhất: model không có trạng thái ẩn. Điều gì cần nhớ sang lượt sau phải là tệp hoặc dòng trong SQLite. Vì vậy có hai chiều:

Vào — system prompt lắp lại mỗi lượt, theo thứ tự cố định. Phần ổn định đứng trước, phần đổi theo lượt đứng sau, để cache prompt của provider giữ được phần đầu:

  1. persona: AGENTS.md, SOUL.md
  2. người dùng: users/owner/USER.md + facts/*.md
  3. MEMORY.md của agent
  4. roster và lệnh kit (.agents/commands/*.md, ví dụ /tongket trong bộ cài thật)
  5. dòng ngày: “Hôm nay: 2026-09-26.” — dòng cuối cùng, vì nó đổi mỗi ngày

Skill không vào toàn văn: chỉ chỉ mục tên + mô tả; agent gọi skill_read khi cần. Skill có always: true thì vào toàn văn.

Trí nhớ đổi theo lượt đi cùng tin mở lượt, không vào system prompt. Đó là hai thứ có thể đổi giữa hai lượt của cùng một cuộc trò chuyện: mục “Cuộc trước (lần cuối 24/9 23:30)”, tức tóm tắt cuộc trò chuyện gần nhất trên cùng kênh (bảng conversations), và ghi chú memory/<hôm qua>.md, memory/<hôm nay>.md. Khi còn nằm cuối system prompt, một lần memory_save đổi văn bản đứng trước cả cuộc trò chuyện, và provider bỏ cache của mọi thứ đã nói. Nay khi lưu một tin mở lượt (tin của run_turn, loạt tin chờ được giao, tin chen ngang), trong cùng giao dịch, hệ thống so từng mục với lần gần nhất cuộc trò chuyện được kể (độ dài và digest của cả mục, đọc lại từ chính các tin đã lưu) rồi lưu kèm tin đúng phần đã đổi, vào cột messages.turn_notes, và không bao giờ viết lại: mục chưa kể thì kể trọn; ghi chú chỉ dài thêm thì chỉ kể các dòng mới; mục bị viết lại thì kể bản mới và nói nó thay bản cũ; mục nay trống thì một dòng nói vậy. Không có gì mới thì tin được đọc đúng như người viết. Model đọc khối đó, trong khung [Bộ nhớ của bạn — hệ thống chèn trước tin này, không phải lời người dùng], ngay trước tin, trước cả ghi chú canvas, ở mọi lần gọi về sau; lịch sử vì thế là một prefix chỉ dài thêm. Mấy điều đi kèm:

Mỗi dòng transcript đưa đi tóm tắt mở đầu bằng ngày giờ theo múi giờ người dùng, và bản tóm tắt ghi ngày cụ thể thay cho “hôm nay”, “hôm qua”: nó được đọc vào một ngày khác.

Ghi chú canvas đi cùng tin của người, không vào system prompt. Khi lưu một tin mở lượt, trong cùng giao dịch, hệ thống so từng canvas gắn với cuộc trò chuyện với bản agent đã thấy hay đã được báo gần nhất, rồi lưu kèm tin một ghi chú: canvas agent chưa đọc lần nào được một dòng; chỗ người tự sửa được một diff, tối đa ba canvas, canvas đổi gần nhất trước; bản do agent khác viết, bản khôi phục, hay một vùng sửa quá dài chỉ được một dòng kèm lời dặn đọc lại. Mỗi diff và cả ghi chú có trần chữ; canvas không vừa được gom vào một dòng cuối. Mọi tin mở lượt, từ web, Telegram, /api/inbound, job hay lượt được giao việc, đều kèm phần đã đổi; chỉ tin từ web chat kèm thêm canvas đang mở và đoạn người đang chọn, vì chỉ ở đó người nhìn thấy canvas. Model đọc ghi chú ngay trước tin trong mọi lần gọi của lượt tin đó mở; từ lượt sau, ghi chú được thay bằng một dòng cố định, nên system prompt và phần đầu prompt mà provider cache vẫn giữ nguyên. Mỗi thay đổi chỉ được báo một lần. Canvas chỉ tính là agent đã thấy bản mới khi diff đi từ bản nó đã thấy trọn và cho thấy mọi chỗ đổi; ngoài trường hợp đó agent phải đọc lại trước khi viết lại cả canvas. Dựng ghi chú lỗi thì tin vẫn được lưu, chỉ không có ghi chú.

Ra — trong lượt:

Cô đọng — job memory_consolidate (cron trong agent.yaml; bộ cài thật đặt một lần mỗi tuần) đọc 7 ngày ghi chú, nhờ model viết lại MEMORY.md, và tạo một đề xuất (memory_proposals). Agent không autonomous luôn để đề xuất chờ người duyệt ở /api/memory/proposals. Agent autonomous chỉ tự áp một đề xuất chỉ thêm dòng hoặc chỉ đổi ngày; đề xuất bỏ hay đổi nội dung một dòng, hoặc mang một ngày không có nguồn, vẫn chờ người duyệt — mã so bản cũ với bản mới quyết định, không phải model. Bản cũ giữ lại để lùi.

Chi tiết ở memory.md.

7. Minh hoạ bằng bộ cài thật

Bộ cài dùng để viết tài liệu này (đã bỏ số liệu và định danh cá nhân):

~/.my-agent-crew/
├── config.yaml            # timezone
├── env                    # OPENROUTER_API_KEY, TELEGRAM_BOT_TOKEN (chmod 600, ngoài repo)
├── agent.yaml             # master "Trợ lý": autonomous, cost_cap_usd, max_steps, telegram
├── agent.sqlite3
├── spill/<conversation>/  # bản gốc của đầu ra tool đã bị rút ngắn; xoá cùng cuộc trò chuyện, quét sau 7 ngày
├── run-server.zsh         # source env → uv run python -m my_agent_crew
├── logs/server.log
├── skills/                # code-review, debug, delegation, git, scout, test
├── .agents/commands/tongket.md
├── users/owner/{USER.md, facts/}
├── workspace/inbox/       # ảnh gửi qua Telegram
└── agents/
    ├── pong/              # AGENTS.md, SOUL.md, MEMORY.md, memory/, agent.yaml
    ├── health-coach/      # như trên
    ├── ledger/            # như trên, workspace = repo sổ cái
    └── fullstack-developer, kongming, researcher

Bốn agent đáng xem kỹ:

Master “Trợ lý” — không có persona dày; việc của nó là hiểu người dùng và chọn người làm. routes một model rẻ, max_steps cao hơn mặc định để đủ chỗ cho vài lần delegate trong một lượt. Là agent duy nhất có telegram:.

Pong — thư ký, chỉ Google Workspace và Goodreads. routes hai model rẻ theo thứ tự fallback. Ba lịch: bản tin sáng, tổng kết tuần, memory_consolidate hàng tuần. Lệnh ghi (gửi mail, ghi sheet, ghi Goodreads) nằm trong shell_ask_patterns nên luôn hỏi.

Ledger — sổ cái tài chính, tách khỏi Pong để agent cầm dữ liệu tiền bạc không có đường nào đưa nó ra ngoài: tools bỏ web_search, fetch_url, delegate, user_memory_save, wiki_*; shell_network: false nên mọi shell_run chạy trong sandbox của macOS: không mạng, không open/osascript, chỉ ghi được dưới shell_write_paths và thư mục temp; shell_ask_patterns chỉ còn là lớp phụ. Ba lịch: bảo trì đêm (lệnh), nhắc hạn (prompt), giá vàng (lệnh). Gọi script của repo sổ cái bằng shell_run — harness chỉ biết cwd và lệnh.

HLV sức khoẻ — routes ba model, tăng dần từ rẻ đến mạnh. Ba lịch: bảo trì đêm, sao lưu, bản tin sáng có gắn skill Garmin. Đọc ảnh bữa ăn qua image_read (tuyến vision_routes). Là ví dụ điển hình của agent “một người dùng, một lĩnh vực, nhớ dài”.

Ba agent còn lại đến từ agent add <template>: fullstack-developer làm trọn việc phần mềm, hỏi kongming (cố vấn chỉ đọc, model mạnh nhất) khi bế tắc và researcher khi cần tra ngoài. Không có lịch, chỉ được gọi qua delegate.

Kit .agents/ cấp home cho master lệnh /tongket — ví dụ về việc harness nạp lệnh kiểu Claude Code làm “prompt có tên” mà không cần code thêm.

8. Những hiểu lầm hay gặp

Hiểu lầm Thực tế trong harness này
“Agent nhớ chuyện hôm qua” Chỉ nhớ nếu có trong MEMORY.md, ghi chú ngày, facts, hay tóm tắt cuộc trước. Không có tệp, không có ký ức.
“Model chạy lệnh shell” Model chỉ xin. Harness chạy, và chỉ khi có duyệt hoặc autonomous.
“Agent con thấy cả cuộc chat” Không. Nó chỉ thấy brief trong delegate. Muốn nó biết gì, master phải viết vào brief.
“Tăng max_steps là agent thông minh hơn” Chỉ cho nó nhiều vòng hơn. Vòng lặp vô tận tốn tiền nhanh; bộ chặn lặp dừng lượt gọi lại y hệt một lệnh, cost_cap_usd là phanh thứ hai.
“Fallback model là im lặng” RouteFallback là event nhìn thấy trên web; run ghi model nào thực sự trả lời.
“Skill là plugin” Skill là tệp markdown. Nó thay đổi lời khuyên cho model, không thêm khả năng mới. Khả năng mới là tool.
“Xoá cuộc trò chuyện là agent quên” Cuộc trò chuyện là log; trí nhớ dài nằm ở tệp. Hai thứ tách nhau có chủ đích.

9. Mở rộng harness

Sáu điểm cắm, theo design.md “Extension points”:

Muốn thêm Làm gì Ở đâu
Model/provider mới một provider biết stream; lắp vào chỗ server dựng provider cho từng agent llm/, server/
Tool mới mô tả cho model + hàm chạy; lắp vào chỗ server dựng bộ tool cho từng agent tools/, server/
Tool của dịch vụ ngoài khai máy chủ MCP trong mcp_servers, bật cho agent bằng mcp:; không cần code config.yaml, agent.yaml
Kỹ năng tệp .md có front matter name, description, always skills/ của home hoặc agent
Agent mới một thư mục có agent.yaml + persona agents/<id>/, hoặc agent add
Kênh mới start / stop / deliver; dựng từ khối trên hồ sơ master channels/

Không có điểm cắm cho “thay vòng lặp”: vòng lặp lượt là bất biến có chủ ý, để mọi kênh, mọi agent, mọi job đi qua cùng một cổng kiểm soát.

Tham chiếu

Câu hỏi mở