Phiên bản: 0.20.0 · Cập nhật: 2026-10-14
Một agent đa năng duy nhất mà một người dùng hằng ngày qua web UI: chat, cho nó dùng tool, duyệt những tool rủi ro, xem nó tốn bao nhiêu. Mọi thứ khác đều phụ thuộc vào vòng lặp đó.
Từ 0.2.0, cùng vòng lặp đó có thể được khởi tạo nhiều lần thành các agent có tên (một HLV sức khoẻ, một trợ lý cá nhân) giữ persona, trí nhớ, workspace và lịch riêng, và UI hiển thị trực tiếp mọi run của mọi agent. Bản thân vòng lặp không đổi; profile và scheduler nằm quanh nó.
| my-crew | Vấn đề đã đo được | Ở đây |
|---|---|---|
| Đội agent theo vai, router, đồ thị task DAG | Phần lớn giá trị đến từ một agent giỏi; code điều phối chiếm phần lớn codebase và danh sách bug | Một vòng lặp agent; profile chỉ thay đổi đầu vào của nó. Không có agent sống lâu nói chuyện với nhau: giao việc là một tool call, sâu một cấp, và cuộc trò chuyện con chạy một lượt rồi trả câu trả lời |
| Profile + YAML công ty + cài đặt theo từng agent | Bề mặt cấu hình quá lớn để giữ được test; secret có thể lọt vào YAML | Biến môi trường + config.yaml theo whitelist; agent.yaml có bộ key cố định và không có secret |
| Web UI thêm muộn, nhiều trang | UI chạy sau tính năng; test là một thế giới riêng | UI là bề mặt chính; ba tầng test dùng chung một hợp đồng event |
| Tệp phình quá 1000 dòng | Khó review, khó cho công cụ LLM | Ngân sách 200 dòng được một test ép buộc, cho cả mã Python lẫn mã nguồn của web |
| Theo dõi chi phí lạc quan | Giá chưa biết bị âm thầm tính là 0 | cost_usd=None được tính vào unknown_cost_calls và hiển thị trên UI |
| Chuỗi tiếng Việt rải rác | Định danh trôi dạt, khó bản địa hoá | Chỉ texts*.py và web/src/i18n/ |
browser ──/api/conversations/{id}/messages (SSE)──┐
Telegram poller ──────────────────────────────────┤
any platform ──POST /api/inbound (JSON, sync)─────┴─▶ Inbound ──▶ agent loop (one turn on one conversation)
│ │ provider chain (ordered routes, fallback before first item)
│ │ tool registry (workspace, web, memory, shell, delegate, canvas)
│ │ skills + persona/memory + crew roster (system prompt)
│ └─ store (SQLite: conversations, messages, approvals, runs, canvases, script_store)
├─ activity hub (live runs → SSE /api/activity/stream)
├─ QueueDrain (what waited while busy → a turn of its own)
└─ Scheduler (cron/every jobs per agent, 20 s tick)
POST /api/inbound nhận TurnReply đã gom (text, status, steps). Không nền tảng nào đặc
biệt, nên một thay đổi ở backend tới được tất cả và một tính năng được test bằng cách post
vào API.TurnHost (turn_host.py) đọc lượt trong một task của
server tới hết; tab gửi tin chỉ là người xem đầu tiên. TurnWatch
(activity/turn_watch.py) phát event của mọi lượt được theo dõi theo cuộc trò chuyện, nên
tab mở giữa chừng — bất kể lượt do tab khác, Telegram hay job khởi chạy — gọi
GET /api/conversations/{id}/turn và nhận khung watching (cuộc trò chuyện như đang lưu
cùng cờ running) rồi phần chữ đang viết và các event tiếp theo; 204 khi không có lượt nào.
Người xem tụt quá 256 event được đưa một khung watching mới thay cho phần bỏ lỡ, nên tab
treo không giữ bộ nhớ vô hạn và không bị cắt. Phần chữ đang viết chỉ nằm trong bộ nhớ: sau
restart, cuộc trò chuyện đã lưu là toàn bộ sự thật.Tin gửi lại không nói hai lần. Mỗi lần gửi từ web mang một request_id (token trơn, tối
đa 64 ký tự). Server lưu tên đó trong cùng transaction với tin nó đặt tên (bảng
message_requests, hoặc cột queued_messages.request_id khi tin còn xếp hàng), nên khi câu
trả lời rớt trên đường về và tab gửi lại cùng tên, server không lưu gì và không chạy lượt
thứ hai: nó đáp bằng chỗ cũ trong hàng, hoặc bằng khung watching và lượt đang chạy. Tên
theo từng cuộc trò chuyện, còn nguyên qua restart, và chưa dùng nếu lần gửi bị từ chối hay
tin bị Dừng rút khỏi hàng. POST /api/inbound không nhận request_id.
queued_messages của cuộc trò chuyện (tối đa 20, tin thứ 21 nhận 429) và người gửi nhận một event
queued (item_id, kind, position) thay cho lượt. Tin thường là follow_up: khi lượt đang
chạy kết thúc, QueueDrain ghép mọi tin đang chờ theo thứ tự, cách nhau một dòng trống, thành một
tin người dùng và chạy một lượt cho nó ở nền, với nguồn của tin đầu. Tin bắt đầu bằng
/steer <chữ> hay một lệnh mà kit của agent định nghĩa là steer: lượt đang chạy lấy nó ở đầu
vòng kế tiếp — sau kết quả của tool đang chạy, trước lần gọi model sau đó — thành một tin người
dùng, hiện trên run là một step steer, và trả lời nó ngay trong lượt ấy. Steer phải chờ tool
đang chạy xong, nên một delegate dài làm nó trễ; steer đến sau lần kiểm cuối của lượt thì ở lại
hàng và được trả lời như tin thường. /tmp/x hay một lệnh không ai định nghĩa là tin thường;
/steer không kèm chữ bị từ chối (422) cả khi rảnh lẫn khi bận, còn lúc rảnh /steer X chỉ là
tin X. Cuộc trò chuyện đang chờ duyệt vẫn từ chối tin mới (409). Một quyết định hay câu trả lời
giữ cuộc trò chuyện ngay lúc được đưa ra, trước khi lượt nó tiếp tục kịp chạy, nên tin gửi ngay
sau đó chờ lượt ấy, còn quyết định thứ hai cho cùng yêu cầu nhận 409.Hàng đợi bền và dừng được. Chuyển tin từ hàng vào lịch sử là một transaction: dòng rời hàng và
tin vào nhật ký trong cùng một commit, nên tin đã báo “đã xếp hàng” không bao giờ mất. Hàng sống
qua restart: khởi động xong, server drain mọi cuộc trò chuyện còn tin, trừ cuộc đang chờ duyệt
(lượt tiếp tục sau quyết định sẽ drain nó). Server chạy --no-schedule không tự bắt đầu việc gì,
nên hàng còn lại nằm đó tới khi lượt kế của cuộc trò chuyện ấy xong. Lúc tắt, drain dừng trước mọi
thứ khác nên không lượt mới nào bắt đầu giữa chừng. Xoá cuộc trò chuyện xoá luôn hàng của nó và, khi lần
xoá đã thành, dừng lượt mà web hay hàng đang chạy cho nó: run ấy đóng là bị ngắt, vì cuộc đã mất
thì không nút Dừng nào còn với tới lượt.
GET /api/conversations/{id} liệt kê tin đang chờ trong queued, và
POST /api/conversations/{id}/stop lấy hết hàng, trả lại chữ trong cleared và huỷ lượt nền mà
hàng đang chạy (cancelled). Nó với tới mọi lượt do server tự đọc: lượt nền của hàng, lượt gửi
từ web (dừng được từ bất kỳ tab nào) và lượt được nối lại sau restart mà không do bot đọc. Lượt
bot Telegram chạy cho tin vừa đến hay lượt của một job đang chạy theo lịch không phải của nó để
dừng, và khi đó cancelled là false. Lệnh xoá cũng không dừng hai loại lượt ấy: chúng chạy tới lần
ghi kế tiếp, và lần ghi đó hỏng vì cuộc đã mất.
store/run_restart.py) và giữ lại trong hub.cut
run running mới nhất, chưa từng được nối, của mỗi cuộc trò chuyện. Trong lifespan của app có
lịch, sau khi bot lên và trước khi hàng đợi drain, resume_cut_turns (turn_resume.py) mở lại
từng run đó — cùng id, cùng timeline, cùng after_seq và số tiền đã tiêu — rồi cho lượt chạy
tiếp từ nhật ký. Hai rào chắn không tắt được: (1) mở lại là ghi resumed xuống SQLite ngay,
trước bước đầu tiên của lượt, nên lượt làm sập server sẽ bị đóng ở lần khởi động sau chứ không
bị thử mãi; (2) lượt nối lại nằm dưới trần chi phí của cuộc trò chuyện — đã hết ngân sách thì
không nối, đang nối mà chạm trần thì dừng như mọi lượt. Run không được nối khi cuộc trò chuyện
đã bị xoá, đang chờ một quyết định (quyết định ấy sẽ nối lượt), đã có run mới hơn, là lượt con
mà lượt giao việc không đi tiếp, hoặc là lượt Telegram mà không có bot nào lên để trả lời; nó ở
lại interrupted và tin kế tiếp thấy nhật ký như sau mọi lượt bị ngắt. Người đọc lượt nối lại
là người vốn đọc loại lượt đó: bot cho lượt Telegram (câu trả lời tới chat), bước giao của
scheduler cho job (đẩy đúng một lần, last_run vẫn là run đó), còn lại là TurnHost nên tab
xem được và Dừng với tới. Bot đọc lượt khi nó chạy và trả lời một lần, nên người đọc cũ mất
cùng tiến trình mang theo đoạn chat chưa từng nhận: đoạn ấy được đọc lại từ nhật ký
(turn_unsent.py) thành đúng những event người đọc cũ đã cầm và đặt trước các event còn tới,
kể từ chỗ lượt bắt đầu hoặc từ lần gần nhất lượt dừng để hỏi người (lời gửi lúc dừng đã nói
hết phần trước đó). Chat nhận trọn câu trả lời, kể cả canvas viết trước lúc cắt, và không nghe
gì hai lần. Lượt giao việc được nối trước lượt con; cha gọi lại delegate, tìm
thấy đúng cuộc con đã mở và chờ nó, hoặc đọc kết quả từ store nếu con đã xong ở tiến trình
trước. Tiền con tiêu vào cuộc cha lúc kết quả giao việc được ghi chứ không phải lúc con xong,
nên lời gọi được gọi lại sau nhát cắt không tính con hai lần. Server chỉ để run ở running cho lần khởi động sau khi chính nó đang tắt
(hub.going_down) hoặc bị giết; Dừng do người bấm, lượt lỗi hay bot khởi động lại khi server
vẫn chạy thì đóng run như cũ. Lúc tắt, bot báo trong chat rằng lượt sẽ được làm tiếp khi
server lên lại. Không có cửa sổ tuổi: lượt bị cắt hôm qua vẫn được nối khi server lên hôm nay.home_lock.py) từ trước khi dựng runtime
tới khi tiến trình mất, kiểu khoá hệ điều hành tự nhả theo tiến trình dù nó kết thúc thế nào:
không tệp nào được tạo, và không tệp nào bị bỏ quên sau một lần sập. Lần khởi động gặp home đang
bị giữ không đọc gì của home, nói home nào rồi thoát mã 1, dù được cho cổng khác hay
--no-schedule. Tiến trình server khởi chạy không được trao khoá, nên một lệnh agent để lại
không giữ home sau khi server mất; các lệnh con agent không giữ gì.Tool.replay_safe (mặc định False) là lời tool khai rằng chạy lại không đổi gì; chỉ mười tám
tool đọc khai nó (đọc workspace, tìm kiếm, web, trí nhớ, wiki, canvas, PDF, ảnh, delegate,
progress_note). replayable (agent/replay.py) quyết từng call: tool lạ hay đối số hỏng,
call còn phải hỏi người, còn chờ hay đã bị từ chối thì chưa hề chạy và được lượt xử như thường;
call đã được cho phép hoặc chạy không cần hỏi thì chỉ gọi lại khi tool khai replay_safe. Các
call còn lại được đóng bằng một kết quả nói nó bị cắt giữa chừng và không rõ đã chạy hay chưa,
để model kiểm rồi quyết — ghi, gửi hay trả tiền hai lần mà không ai thấy là điều phải tránh.
Tool mới (kể cả tool từ nguồn ngoài) không khai gì thì được coi là đổi trạng thái.
SQLite mở ở chế độ WAL với synchronous=NORMAL (store/connection.py): mỗi commit chỉ
nối vào log thay vì ép tệp chính xuống đĩa, nên hàng chục lần ghi nhỏ của một lượt rẻ;
mất điện có thể mất vài commit cuối nhưng không hỏng tệp, crash tiến trình không mất gì.
Ghi rồi đọc lại dùng RETURNING trong một câu lệnh. Đọc tệp live từ ngoài thì mở bằng
mode=ro (không phải immutable=1, vì bản đó không thấy phần còn nằm trong log WAL).store/search_index.py dựng bảng FTS5 messages_fts và
trigger giữ nó khớp messages (insert/delete, không update vì messages chỉ thêm) trong
cùng transaction với apply_schema, nên một crash giữa chừng không để lại chỉ mục rỗng mà
lần khởi động sau tưởng đã xong. Bộ gõ unicode61 remove_diacritics 2 tự bỏ dấu khi so
khớp nhưng không đụng tới đ; trigger và backfill tự thay đ/Đ bằng d/D trước khi
đưa vào chỉ mục, để gõ “doc” vẫn khớp “đọc”. store/search.py là nơi cả conversation_search
(tool cho model, phạm vi theo agent) lẫn GET /api/messages/search (cho người, ở phần Web UI
bên dưới) cùng đọc.requires_approval tạm dừng lượt với một event approval_required
và một Approval được lưu; UI hiện một thanh, endpoint quyết định tiếp tục đúng lượt đó.
Cuộc trò chuyện đánh dấu autonomous bỏ qua chỗ dừng. Các từ chối cứng — path thoát khỏi
workspace, đích mạng private/loopback — không duyệt được.Approval mang một hạn chót
(approval_ttl_seconds, mặc định 600; một lịch hay khối telegram đặt được hạn riêng, cuộc
trò chuyện chúng mở giữ hạn đó và cuộc của agent con chép của cha); tick của scheduler quét những cái quá hạn,
đóng chúng là expired, tiếp tục lượt với tool bị từ chối
và gửi câu trả lời như mọi lượt khác, nên một yêu cầu không ai thấy không bao giờ treo
cuộc trò chuyện. Endpoint quyết định cũng nhận always: duyệt kèm nó thì tool được thêm vào
danh sách auto_approve của cuộc trò chuyện và các lần gọi tool đó về sau chạy không cần hỏi,
cho tới khi chip trên header thu hồi. Danh sách hỏi của shell vẫn tạm dừng một shell_run
đã được cho phép luôn. Các yêu cầu đã quyết vẫn đọc được ở GET /api/approvals.POST /api/conversations/{id}/fork (thân
{before_message_id}) tạo một hội thoại mới chép mọi tin trước điểm cắt bằng một câu
INSERT … SELECT giữ nguyên role, content, tool_calls, tool_call_id, name, model,
created_at và đánh lại seq liền mạch từ 1; điểm cắt phải là một tin role == "user" đã lưu
của chính hội thoại đó (tin còn trong queued_messages không có), và hội thoại con của delegate
bị từ chối vì tin đầu của nó là lời agent cha. provider, cost_usd và bốn cột token luôn NULL
trên bản chép — tiền đã tiêu thuộc về gốc, by_model/by_purpose/by_day không đếm hai lần —
và nhánh có spent_usd = 0, autonomous theo mặc định của agent, auto_approve = (): quyền
không đi theo lịch sử, vì mở lại đúng chỗ thường là để agent làm khác đi. Một tool call còn mở
ở cuối phần chép (lượt bị ngắt giữa chừng rồi người nhắn tiếp, nên lịch sử thật có dạng
[assistant(A,B), tool(A), user] với B chưa trả lời) được refuse_unanswered đóng lại ngay sau
khi chép, ghi kết quả từ chối mà không chạy tool — nếu không, settle_tool_calls ở lượt đầu của
nhánh sẽ tự chạy lại một tool đổi trạng thái ngoài ý chủ. Toàn bộ (tạo, chép, đóng call mở) nằm
dưới một lần giữ lock; lỗi ở bất kỳ bước nào xoá nhánh vừa tạo rồi ném lại lỗi, nên không bao giờ
còn một nhánh nửa chừng. Xoá hội thoại gốc chỉ gỡ liên kết forked_from của các nhánh, không xoá
chúng. Đánh đổi đã ghi nhận: con delegate và MAX_DELEGATES được tính theo lịch sử nên nhánh
thấy (và bị tính) con của gốc; kết quả tìm kiếm ở phần chữ chung giữa gốc và nhánh chỉ về một hit,
thuộc về nhánh vì id lớn hơn.route_fallback
và ghi thành step fallback trên run, nên một model cứ lỗi mãi sẽ hiện trên timeline
thay vì âm thầm tốn thêm ở tuyến kế tiếp. Step fallback mang thời gian của lần thử hỏng
đó; step model mà request đã mở dời ra sau nó để đo tuyến kế tiếp, nên run hồi phục không
để lại step model nào mở, còn khi mọi tuyến đều hỏng thì run chỉ còn các step fallback.delta.reasoning và phát event thinking một lần mỗi lần
gọi model. Web hiện “Agent đang suy nghĩ…” còn step model trên run tính cả quãng im lặng
đó. Nội dung suy nghĩ không hiện và không lưu, chỉ lưu số reasoning_tokens.provider_unavailable).
Lỗi tạm thời là HTTP 408/425/429/5xx, chunk lỗi mang các mã đó hoặc provider_unavailable,
và lỗi kết nối; request bị từ chối (4xx khác), chunk lỗi không mã và stream hỏng thì không
thử lại. Chuỗi chờ 2 giây rồi hỏi lại cùng tuyến (ProviderChain.retries = 1), phát item
RouteRetry và log retrying. Chỉ thử lại khi chưa có TextDelta: mốc thời gian và suy nghĩ
không bao giờ hiện, nên lần hỏi mới không để lại đường nối; đã có chữ thì lỗi vẫn đưa lên bề
mặt như cũ. Hỏi lại vẫn hỏng thì mới rơi sang tuyến kế (hoặc báo mọi tuyến hỏng). Đây không
phải tuyến dự phòng: cùng model, cùng provider, nên quyết định một tuyến của chủ vẫn nguyên.
Lời gọi bên cạnh lượt bị bỏ dở sau chunk đầu rồi được hỏi lại vẫn ghi vào sổ với giá không rõ.escalation_route, một provider:model chỉ dùng khi lượt của nó
kẹt — sắp bị bộ chặn lặp dừng, hoặc mọi tuyến chính đều hỏng trước khi hiện chữ nào. Tắt theo
mặc định, chỉ đặt cho từng agent, không thừa hưởng. Lượt chuyển tối đa một lần, phần còn lại
chạy trên tuyến đó, lượt sau quay về tuyến chính; hết bước và hết ngân sách không bao giờ leo
thang. Một tuyến trùng tuyến chính hay thiếu khoá không phải là lối ra nên không được dùng. Việc
chuyển luôn nhìn thấy được: event escalated, một dòng báo trên web và step escalation trên
run, để chi phí của model đắt hơn không bao giờ đến âm thầm.config.yaml: không đối số tool nào
và không câu trả lời nào của máy chủ chọn được nơi một request đi tới. Chỉ streamable HTTP;
stdio là chạy chương trình của người khác bằng quyền của crew nên không có. Mọi tool MCP hỏi
trước khi chạy và không bao giờ được gọi lại sau một lần restart, cho tới khi chủ ghi nó vào
read_only; lời máy chủ tự nhận “chỉ đọc” được hiện ra và không quyết định gì. Tệp cấu hình
không giữ khoá: header lấy giá trị từ môi trường, còn token đăng nhập nằm cùng chỗ với khoá
provider và không API nào trả lại. Đăng nhập OAuth chỉ bắt đầu được từ trình duyệt trên chính
máy chạy crew, chỉ với địa chỉ https công khai, không đi theo chuyển hướng. Một máy chủ hỏng
là một dòng nói vì sao ở Kết nối, không phải một crew không khởi động được. Tool MCP theo mcp:
của agent chứ không theo tools, và mặc định không khai sẵn cho model (deferred) để một máy
chủ vài chục tool không đội giá mọi lượt. Agent tìm chúng bằng tool_search, một lần tìm BM25
trên những tool nó đang giữ, không gọi máy chủ nào; tool tìm thấy được khai từ lời gọi model kế
tiếp, nối vào cuối danh sách đã khai để phần đầu request mà provider cache không đổi. Cái đã
nạp không nằm trong bộ nhớ của server mà trong chính hội thoại (các câu trả lời tool_search
đã lưu), nên một lần restart không làm mất nó. Client viết tay trên httpx, không thêm SDK.
Việc phải gọi một tool nhiều lần, hay chỉ cần vài dòng của một câu trả lời dài, agent viết
thành script cho tool_script: script gọi tool và chỉ phần nó in ra mới vào ngữ cảnh. Script
chỉ gọi được thứ chỉ đọc và không hỏi ai (tool có sẵn tự khai gọi lại được, và tool MCP chủ
ghi read_only mà không ẩn), nên nó không bao giờ đứng chờ duyệt và mọi lời gọi có
ghi vẫn qua cổng duyệt. Cũng vì thế agent nào giữ ít nhất một tool như vậy đều có
tool_script, không cần máy chủ MCP nào và không theo tools hay mode: đọc mười tệp rồi
lọc lấy vài dòng cũng là việc của script. Chủ có thể mở rộng điều đó cho từng agent
(script_calls: gated trong agent.yaml của nó, mặc định tắt): script của agent ấy gọi được
cả tool có ghi, mà hai điều trên vẫn giữ. Cổng duyệt xét từng lời gọi như vậy trước khi nó
chạy, bằng đúng hàm xét một lời gọi trực tiếp, và lời gọi nào phải hỏi người thì script dừng
ở đó với lời bảo gọi trực tiếp: một lượt đang chờ duyệt không giữ gì trong bộ nhớ, nên script
không có cách dừng giữa chừng để hỏi. Script như thế bị restart cắt thì không chạy lại, vì
không gì nói nó đã ghi những gì. Ngôn ngữ là một phần của Python do chính crew thông
dịch, không eval, không import, không đọc thuộc tính; script chạy trong một tiến trình con
với môi trường rỗng và các giới hạn cứng, trên macOS trong sandbox không mạng, không ghi,
không fork. Việc code không quyết nổi (xếp một văn bản vào một trong vài nhãn, rút ngắn nó)
script hỏi model bằng llm.classify và llm.summarise. Tiến trình con không cầm khoá và không
ra mạng, nên nó chỉ gửi câu hỏi; server gọi model của chính agent, trên route đầu tiên và chỉ
route đó, không khai tool nào, rồi trả về đúng một chuỗi. Văn bản được đưa cho model như dữ
liệu trong một hàng rào, nhưng thứ thật sự giữ là cách server đọc câu trả lời: phân loại chỉ
trả về một nhãn script đã đưa, tóm tắt bị cắt ở độ dài script xin. Mỗi lần hỏi là một lời gọi
của script, đếm và tính tiền như một lời gọi tool có trả tiền cho model. Script của agent
script_calls: gated còn giao việc được cho agent khác (tools.delegate), chỉ trong lượt của
chính agent: không trong lượt của một việc được giao, nên độ sâu vẫn dừng ở một, và không
trong job, nơi không có cuộc trò chuyện nào để làm cha. delegate tự khai là chỉ đọc, nhưng
nó đặt một agent khác vào việc bằng tiền của cuộc trò chuyện, nên script gọi nó theo một luật
riêng chứ không theo phép xét “chỉ đọc”. Mỗi việc chạy dưới một id lời gọi riêng (<id lời
gọi tool_script>:<số>), để hai việc của một script mở hai cuộc trò chuyện và script chạy lại
dưới cùng id tìm lại chúng. Không tin nhắn nào của cha mang id đó, nên mọi chỗ cần biết một
cuộc trò chuyện đã giao những gì (mức tám việc, hoạt động của cuộc trò chuyện, bản xuất quỹ
đạo) tìm con qua một đường duy nhất trong kho: theo id của lời gọi delegate, và theo id của
lời gọi tool_script mà id của con mở đầu bằng. Tiền các con tiêu nằm trên kết quả của
script, nên cuộc trò chuyện cha được cộng một lần.cost_usd hoặc None. Cuộc trò chuyện giữ
spent_usd và unknown_cost_calls; cost_cap_usd dừng trước lần gọi model kế tiếp
(0 = không giới hạn). Mọi lần gọi model nằm ngoài vòng lặp lượt — đặt tiêu đề, tóm tắt
phiên, tóm tắt output tool dài, đọc ảnh, đọc trang PDF scan, cô đọng bộ nhớ, dựng wiki, lần
một script tự hỏi model (mục đích script) —
đi qua MeteredChain và để lại một dòng trong bảng side_calls kèm mục đích của nó (chỉ số
liệu, không bao giờ prompt). Sổ cái sử dụng (/api/stats: days, models, purposes) cộng
hai bảng đó và không đọc run, nên không lần gọi nào bị đếm hai lần hay bị bỏ sót. Tool có gọi
model (ảnh, PDF scan, bản tóm tắt được dùng, lần script tự hỏi model) được tính vào run và cuộc
trò chuyện như một completion; giá không rõ được đếm riêng, không bao giờ coi là 0. Lần gọi bị bỏ dở sau chunk
đầu tiên vẫn được ghi, với giá không rõ. Bản tóm tắt hết giờ hay rỗng — tool rơi về cắt —
chỉ nằm trong sổ cái: run và cuộc trò chuyện không có con số nào để cộng.MY_AGENT_ROUTES=fake:echo chạy cả stack mà không cần
key; /tool <name> {json} điều khiển tool thật qua đường duyệt thật. Đó cũng là thứ
live smoke và test trình duyệt dựa vào.tool_output_chars.MY_AGENT_HOME/agents/<id>/agent.yaml mô tả một agent: bộ key cố định, không có secret,
mọi giá trị chưa đặt kế thừa từ cài đặt toàn cục. Agent default luôn tồn tại và chính là
cài đặt cấp cao nhất, nên một home mới không cần profile. Tệp persona và trí nhớ là Markdown
trong thư mục agent, được đọc vào system prompt mỗi lượt. Tham chiếu key và bố cục thư mục:
agents.md; tệp trí nhớ và tool: memory.md; bộ tool và giới hạn
của nó: tools.md.
Chế độ work. mode: work đổi các mặc định sang những gì một job lập trình cần — trần chi
tiêu cao hơn, nhiều step hơn, và bật autonomous — vì người yêu cầu refactor không ngồi đó để
duyệt từng lần ghi tệp. Đó là một mặc định khác, không phải một luật khác: các tool luôn hỏi
vẫn hỏi, và trần vẫn dừng lượt. Agent work cũng nhận delegate trừ khi danh sách cho phép
tools của nó bỏ tool này ra, nên một chuyên gia vẫn là chuyên gia thay vì âm thầm lập một
đội riêng.
Master. Agent default là agent mà người dùng nói chuyện. Nó mang delegate và, trừ khi
MY_AGENT_HOME/agent.yaml tuỳ chọn của nó nêu một danh sách delegates, có thể gọi tới mọi
agent khác trong home; system prompt của nó liệt kê đội đó mỗi lượt. Điều này giữ sản phẩm là
“một agent giỏi, tự chủ” trong khi vẫn cho nó bố trí người cho một job: người dùng không chọn
agent, master chọn — trên web cũng như trên Telegram. Đội giữ lịch của mình và đơn giản là
cũng có tên trong danh sách. Khi một lượt của master chỉ là một lần delegate và việc được giao
đi tới done (tools/delegate_outcome.py: dữ kiện của runtime trước, rồi dòng Status: con
khai), câu trả lời của con được chuyển nguyên văn cho người dùng (agent/delegate_relay.py) thay
vì tốn thêm một lần gọi model để kể lại; agent con, từ lần gọi thứ 25, được nhắc kết luận và
không còn tool (agent/child_wrap_up.py) để không bị trần bước cắt giữa chừng. Chi tiết:
agents.md.
Mẫu. Ba profile đi kèm ứng dụng — một lập trình viên, một cố vấn và một nghiên cứu viên — cài bằng
agent add <id> hoặc POST /api/agents/install (thứ mà tab đội gọi), kéo theo các agent
đồng cấp mà vai đó giao việc cho và một bộ skill dùng chung. Mọi manifest đều chạy được ngay
như khi cài (workspace chung của home, các tuyến toàn cục); --workspace ghim một vai vào
một repository. Cài qua API thì gia nhập đội đang chạy ngay lập tức, còn lịch thì khởi động
lúc boot. Chúng là điểm xuất phát để sửa, không phải framework: mỗi cái là một
agent.yaml với cùng bộ key cố định, nên không có gì phải học ngoài định dạng profile.
Kit. Người đã dùng Claude Code hoặc opencode có sẵn một .claude/ hoặc
.opencode/ đầy subagent, command, skill và hook. Thay vì một công cụ di trú,
đội đọc nguyên các thư mục đó: .agents/, .claude/ và
.opencode/ dưới home và dưới thư mục agent. Agent Markdown thành thành viên đội,
tệp command thành slash command, hook chạy với cùng hợp đồng JSON và cùng tên tool qua một
bảng alias. Profile yaml giữ tiếng nói cuối với bất kỳ id nào cả hai cùng định nghĩa. Kit nằm
trong workspace mà agent làm việc thì cố ý không được đọc: repository đó là nguồn dữ liệu cho
đội, và .claude/ của nó thuộc về người phát triển nó. Chi tiết:
agents.md.
Trí nhớ là Markdown trên đĩa ở hai phạm vi: những gì đội biết về người dùng
(users/owner/, mọi agent đều đọc) và những gì một agent biết về công việc của chính nó
(MEMORY.md cùng ghi chú theo ngày trong thư mục của nó). Tham chiếu đầy đủ: memory.md.
Hai phạm vi thay vì một, vì hai bên có người đọc khác nhau. Một sự thật về người dùng — họ thích được trả lời thế nào, họ đang làm gì — mà phải học lại ở từng agent là sai: nói với HLV một điều mà trợ lý không biết chính là lỗi mà cách này sửa. Ghi chú công việc thì ngược lại: số đo của HLV sẽ là nhiễu trong prompt của trợ lý, và ghi chú của mọi agent dồn vào một tệp sẽ vỡ trần của section.
Ghi khi người dùng có mặt thì vào ngay; ghi từ một job không ai trông thành đề xuất trong
memory_proposals. Ranh giới là ai có thể phản đối, không phải lần ghi trông rủi ro tới đâu:
một job theo lịch ghi đè profile của người dùng bằng một phỏng đoán sai thì không ai bắt được.
Cùng lý lẽ đó khiến hợp nhất — lần ghi lại MEMORY.md theo lịch từ các ghi chú gần đây —
là một đề xuất giữ lại văn bản nó thay thế, nên lùi một bước luôn khả thi.
Phần đổi theo lượt đứng cạnh tin, không đứng trong system prompt. Ghi chú hôm qua, hôm nay
và tóm tắt cuộc trước từng khép system prompt. Đo trên mười ngày của bộ cài thật: lần gọi model
ngay sau một memory_save chỉ còn 15,1% token prompt được provider tính giá cache, so với
85,5% ở các lần gọi khác, vì một dòng ghi chú mới đổi văn bản đứng trước toàn bộ cuộc trò
chuyện. Số lần như vậy ít (4 trên 334 lần gọi, khoảng 1,6% token prompt), nên đây là sửa một
chỗ sai về cấu trúc hơn là một khoản tiết kiệm lớn. Nay hai thứ đó được lưu cùng tin mở lượt và model đọc ngay trước tin ấy, mỗi lần chỉ
phần đã đổi, nên system prompt giống nhau từ lượt này sang lượt khác và lịch sử là một prefix
chỉ dài thêm. Ba lựa chọn đáng ghi lại. Dòng ngày ở lại cuối system prompt: nó đổi một lần mỗi
ngày, mất cache một lần mỗi ngày là cái giá nhỏ để model luôn thấy ngày ở một chỗ cố định. Khối
được lưu lúc INSERT và không bao giờ sửa, đúng như ghi chú canvas, nên những gì model đã đọc
không đổi dưới chân nó và một cuộc trò chuyện đang dở lúc nâng cấp vẫn đọc đủ trí nhớ (cuộc
không có tin nào mang khối thì đọc bản hiện tại trước tin đầu). Và một lượt không đọc lại
ghi chú chính nó vừa ghi: nó đã biết qua lần gọi tool, còn chèn lại trước tin đang mở lượt
chính là thứ làm vỡ cache. Cách dựng khối:
system-architecture.md.
Mọi lượt model — chat, tiếp tục sau duyệt, prompt theo lịch, command theo lịch — là một run.
Run ghi agent của nó, nguồn (chat, job:<id>),
cuộc trò chuyện, trạng thái, các step (lần gọi model kèm chi phí, tool call kèm kết quả và thời lượng), chi tiêu
và một tóm tắt. Run đang chạy được giữ trong bộ nhớ và phát dạng SSE trên /api/activity/stream
(snapshot khi kết nối, rồi các frame run và event); run đã xong được đọc từ SQLite. Cùng luồng ấy
còn mang conversation và artifact, mỗi frame kèm bản ghi vừa đổi, và jobs, không kèm gì:
nó chỉ báo rằng danh sách job đã khác lần đọc trước, vì chuỗi hỏng của một job vừa được đếm xong,
vì đồng hồ vừa đọc lại lịch của đội, hay vì một agent vừa rời đội (channels.md,
bullet “Lịch hỏng mãi thì không nhắn mãi” và các bullet sau nó).
Khi server khởi động, run còn đánh dấu đang chạy bị đóng là error với tóm tắt
interrupted; run mới nhất chưa từng được nối của mỗi cuộc trò chuyện được trao lại để
server nối tiếp đúng một lần (mục “Lượt bị restart cắt được nối lại một lần” ở trên), và
khi đó /api/activity/runs và luồng activity trả nó với resumed: true để web ghi “Tiếp tục sau khi server khởi
động lại” dưới thanh tiến độ. Run đang dừng chờ duyệt mà cuộc trò chuyện còn một yêu cầu chưa quyết thì
được giữ lại trong bộ nhớ, nên quyết định đến sau lần khởi động lại chạy tiếp chính run đó
thay vì mở run thứ hai; run chờ duyệt không còn yêu cầu nào để chờ (yêu cầu đã được quyết
khi không tiến trình nào giữ run) bị đóng như trên, và xoá một cuộc trò chuyện cũng đóng
ngay run đang chờ duyệt trong đó. Run được ghi và phát ở ranh giới step: token stream (text_delta,
thinking) chỉ cộng dồn vào step đang dựng trong bộ nhớ, không ghi SQLite và không lên
luồng activity (rail không hiện từng chữ). Mảnh đối số của một lời viết canvas
(tool_call_delta) cũng là event chỉ stream: nó tới request đang stream lượt ấy để tab
thấy tài liệu hiện dần, không đổi run, không ghi SQLite và không lên luồng activity. Tắt
xem trước trong Cài đặt chỉ tắt việc hiện ở thiết bị đó: server vẫn gửi các mảnh.
Mỗi watcher có hàng đợi 256 frame; một tab
ngừng đọc bị cắt và trình duyệt kết nối lại với snapshot mới, thay vì giữ mọi event
của mọi run trong bộ nhớ server. /api/stats giữ câu trả lời cuối cùng theo số lần ghi
của store và chỉ tính lại khi có gì đó được ghi.
scheduler/ biến mỗi lịch được bật thành một job <agent_id>/<schedule_id>. Một lịch có
đúng một trong cron (năm trường, theo múi giờ của người dùng) hoặc every (30m, 2h, 1d) và đúng
một trong prompt, command hoặc script. Job prompt mở một cuộc trò chuyện autonomous mới cho agent
và chạy một lượt; job command chạy chuỗi lệnh bằng shell_run trong workspace của agent và
chỉ ghi step đó; job script chạy một script của tool_script mà không mở lượt nào của agent
(scheduler/script_job.py, qua cùng run_source với script của một lượt; model chỉ được gọi
khi chính script hỏi nó bằng llm.): run của nó không gắn
cuộc trò chuyện nào và có đúng một step tool_script, cổng duyệt coi nó là việc không ai trông
nên không gì chờ người, và chỉ khi script chạy xong, có in ra gì đó và lịch có then_prompt thì
một lượt mới được mở như job prompt mở, mang then_prompt và cái script in ra, đặt giữa hai dòng
đánh dấu có số tạo riêng cho tin đó kèm một câu nói đó là dữ liệu chứ không phải chỉ dẫn (chi tiết ở
agents.md); job consolidate, thêm bởi một cron memory_consolidate, đề xuất viết
lại MEMORY.md của agent bằng một lần gọi model (master gọi thêm một lần để xem lại fact người
dùng) và không mở cuộc trò chuyện nào, nên không gửi gì cả. Tick là 20 s; POST /api/jobs/{id}/run khởi động một job ngay lập tức và
trả 202. Đồng hồ của lịch là múi giờ của người dùng: key timezone trong config.yaml (hoặc
MY_AGENT_TIMEZONE), một tên IANA như Asia/Ho_Chi_Minh, và múi giờ của máy khi chưa đặt.
Mọi dấu thời gian trong database vẫn là UTC và được đổi sang ngày của người dùng cho
prompt, /status, thống kê activity và sổ cái sử dụng.
PATCH /api/jobs/{id}/state tạm dừng hoặc tiếp tục một lịch lúc chạy; ghi đè này được lưu
trong database, sống qua restart và chỉ áp dụng với lịch
mà profile bật — lịch tắt trong yaml được báo là enabled: false, paused: false và
không bật được từ UI. GET /api/jobs/{id}/runs liệt kê các run đã qua của job đó.
Sổ của script. Bảng script_store (agent_id, namespace, key, value_json,
updated_at; khoá chính là ba cột đầu) giữ những gì một script ghi bằng store để lần chạy sau
đọc bằng load. Script nêu khoá, còn namespace do server chọn: job:<schedule_id> cho job
script và cho lượt mà một job mở, conv:<conversation_id> cho mọi lượt khác, và dòng nào cũng
thuộc riêng một agent. Lần ghi của một script chỉ được áp khi nó chạy xong, tất cả trong một
transaction, và kho được đo lại ngay trong transaction đó: ghi ngay từng lần thì một script hỏng
giữa chừng để lại nửa sổ, một trạng thái mà không lần chạy nào từng muốn có. Kho conv: bị xoá
trong cùng transaction với cuộc trò chuyện của nó (Store.delete). Chi tiết và giới hạn ở
tools.md.
Lịch trong tệp theo đội đang chạy. Job sinh từ schedules và memory_consolidate được đọc
từ chính các agent đang chạy, và đọc lại mỗi khi đội đổi: một agent được lưu, được cài, được nhận
vào đội, hay cả đội được dựng lại sau một lần lưu ở Kết nối (Scheduler.reload_profiles, gọi từ
server/runtime_crew.py và server/runtime_connections.py). Vì vậy không lần lưu nào chờ khởi
động lại, và restart_required của route sửa agent luôn rỗng. Bảng mới được đặt vào thay cho
bảng cũ chứ không sửa tại chỗ, vì describe() chạy trong thread pool có thể đang đọc bảng cũ; cũng
vì thế mốc “lần cuối” của một job đã bị bỏ không bị xoá. Mốc được xét theo giờ chứ không theo nội
dung: dòng mới, dòng đổi cron hay every và dòng tệp vừa bật lại nhận mốc là lúc lưu, nên không
giờ nào đã qua được chạy bù; dòng chỉ đổi việc nó làm giữ mốc, nên sửa một prompt không dời lần
chạy kế tiếp. Dòng bị bỏ mất công tắc job_state và số lần hỏng đang đếm, để một dòng thêm lại
dưới cùng id không thừa hưởng gì. Trong một nhịp, job tới hạn mà dòng của nó vừa được lưu hay bị
bỏ khi một job trước nó đang chạy thì bị bỏ qua và nhịp sau xét lại: chạy nó theo dòng cũ là chạy
thứ người dùng vừa sửa đi. Quy tắc đầy đủ ở
agents.md.
Lịch từ chat. Bên cạnh lịch viết tay trong agent.yaml, tool schedule_create
(origin: "chat" trên job, đối lập origin: "profile") cho agent tự đề xuất một job prompt
lặp lại; chi tiết tool ở tools.md. Lịch loại này sống trong
bảng created_schedules, không phải agent.yaml, vì yaml thuộc home mà phiên khác quản lý và
một tệp yaml ghi ngoài đường lưu của web thì đội đang chạy không đọc lại — id có tiền tố chat- nên không bao giờ trùng id viết tay. Scheduler
không giữ một bảng job trong bộ nhớ cho nguồn này; nó dựng lại {job từ profile} ∪ chat_jobs(DB)
mỗi lần jobs(), describe(), due() hay get() cần, vì bảng trong bộ nhớ phải được cập
nhật cả từ tool (chạy trước khi Runtime/Scheduler tồn tại) lẫn từ route job (đồng bộ, chạy
trong thread pool) trong khi tick chạy trong event loop — hai luồng cùng sửa một cấu trúc tại
chỗ là đua dữ liệu, còn dựng lại từ DB mỗi lần thì không. Hệ quả: mốc “lần cuối” của một lịch
mới là lúc scheduler thấy nó lần đầu, nên lần chạy rơi vào khoảng 20 giây đầu (một nhịp tick)
có thể bị lỡ; khởi động lại không làm mất lịch vì mọi thứ đã ở DB. Xoá qua
DELETE /api/jobs/{id:path} bỏ dòng created_schedules và job_state liên quan, giữ nguyên
lịch sử run, và không đi qua Scheduler để dòng mồ côi (agent gọi nó đã bị gỡ) vẫn xoá được;
xoá một job có nguồn profile bị từ chối với 409, vì nó là một dòng của agent.yaml: muốn bỏ
thì lưu agent không còn dòng đó, còn gỡ riêng khỏi đồng hồ thì lần khởi động sau nó lại hiện ra.
Duyệt luôn luôn cho lịch chạy. Tạo một lịch nghĩa là agent tự cho mình một lượt chạy
trong tương lai không ai trông chừng — đúng loại việc mà duyệt tồn tại để chặn — nên
schedule_create phải dừng chờ người ngay cả khi mọi cơ chế miễn duyệt khác đều cho qua:
cuộc trò chuyện autonomous, tool nằm trong auto_approve, hay tool nằm trong allow-list của
agent. Tool có trường ask_reason: Callable[[dict], str] | None; khi khác None,
tool_gate.ask_reason_for/ask_reason_text trả nguyên văn kết quả của nó trước khi xét tới
allow-list, autonomy hay auto_approve, và luật 2 của needs_decision
(if reason: return True) khi đó luôn đúng. Cổng đóng khi lỗi: reason rỗng, hay tool ném lỗi
giữa lúc dựng reason, vẫn dừng chờ người — thẻ khi đó chỉ ghi tên tool — thay vì để lời gọi
lọt qua cổng hay làm đổ cả lượt. Không có hằng số kiểu ALWAYS_ASK, không import
scheduler vào agent/tool_gate.py, và không công tắc nào trong agent.yaml hay
config.yaml tắt được luật này — nó chỉ đổi được bằng cách sửa code. Vì thẻ duyệt là nơi
duy nhất người đọc nguyên văn prompt trước khi nó chạy nhiều lần sau này mà không ai hỏi lại
từng bước, nút “Luôn cho phép” bị ẩn trên mọi thẻ có reason (kể cả thẻ lệnh shell khớp mẫu
hỏi): bấm nó không bỏ được chính lần hỏi ấy mà chỉ lặng lẽ thêm tool vào auto_approve, miễn
duyệt các lời gọi khác của cùng tool.
channels/ cho người dùng nói chuyện với đội trên thứ khác ngoài web UI. Hiện nay
đó là Telegram: agent.yaml của master có telegram: {token_env, chat_id} được một
bot lúc khởi động khi biến môi trường được nêu tên đã đặt, dựng lại tại chỗ
khi khối trong profile hoặc token của nó đổi từ web UI. Chat đó là
cuộc trò chuyện của master, nên điện thoại và web UI là cùng một cơ chế: một agent đứng ở
cửa, giao việc phía sau. Lượt đi qua cổng inbound như mọi nền tảng khác, với
nguồn telegram, câu trả lời gửi về dạng text và sendPhoto, slash command được trả lời
không cần gọi model, và scheduler đẩy câu trả lời của job prompt của bất kỳ agent nào
tới chat dưới tiền tố [Name], với MEDIA: của nó đọc từ workspace của agent đó.
Hành vi đầy đủ, lệnh, offset và secret: channels.md.
shell_run thực thi một lệnh trong workspace của agent với môi trường tối thiểu
(PATH, HOME, LANG, TERM, TMPDIR, USER, SHELL) và timeout có giới hạn; nó luôn cần duyệt
trừ khi cuộc trò chuyện hoặc agent là autonomous. GET /api/agents/{id}/files?path= chỉ phục vụ tệp
từ bên trong workspace đó, và đó là cách một dòng MEDIA: charts/sleep.png của assistant được
UI hiển thị inline. Tệp ở đó là thứ agent viết, mà agent có thể bị dắt bởi thứ nó đọc, nên route
phục vụ nó như nội dung không tin cậy. Chỉ ảnh raster, PDF và chữ thuần mở tại chỗ; mọi tệp khác
tải về dưới dạng byte. Mọi response trừ PDF có CSP sandbox không cho chạy script và đặt trang
vào origin mờ, nên request từ đó mang Origin: null và bị rào Host/Origin từ chối; cùng với
nosniff và Cross-Origin-Resource-Policy: same-origin. Bảng đuôi ghim trong code, không theo
bảng MIME của máy, nên máy nào cũng ra cùng header.
Ảnh đi chiều ngược lại qua image_read: model chat trên các tuyến của agent được
chọn vì giá và văn bản, nên tool gửi tệp xuống một chuỗi vision_routes riêng
kèm một câu hỏi và trả câu trả lời dạng text. Agent nào cũng có nó, master đọc một lần
để định tuyến bức ảnh và chuyên gia đọc lại để lấy chi tiết mình cần, và lần gọi
được tính vào cuộc trò chuyện như một completion. tools.md.
Một số việc không có API: đơn hàng nằm sau một trang đã đăng nhập, một trang chỉ vẽ nội dung
bằng script. fetch_url không tới được những chỗ đó, nên agent được trao một trình duyệt thật.
Những lựa chọn dưới đây đều đi từ cùng một câu hỏi: trang web là thứ người lạ viết, và agent
đọc nó với phiên đăng nhập của chủ.
<home>/browser/<agent id>/. Nó không thấy cookie của chủ, và agent này không
thấy phiên đăng nhập của agent kia. Chủ đăng nhập cho agent nào thì agent đó có đúng phiên
ấy. Gắn vào Chrome đang mở của chủ thì rẻ hơn nhưng trao cả mọi tab và mọi phiên; Chrome
cũng đã chặn việc điều khiển hồ sơ mặc định từ xa.browser_enabled trên máy và khoá browser trong tệp của từng agent,
không thừa hưởng từ master. Trình duyệt chỉ có ở agent mà có người đã viết ra như vậy, và
danh sách tools: không đem nó tới: một agent không thể tự có trình duyệt nhờ một cái tên
trong danh sách.ref. Nó rẻ hơn HTML nhiều lần, không cần model đọc ảnh, và cho cổng duyệt một thứ để
xét: tên của nút sắp bấm. Ảnh chụp là đường phụ, cho trang mà dàn ý không nói đủ.click e12 không ai duyệt
nổi. Vì vậy browser_act bắt model nêu tên phần tử, từ chối khi tên ấy không còn đúng với
trang, và dừng lại hỏi người khi tên mang một từ như “thanh toán”, “xoá”, “gửi”, kể cả ở
agent tự chủ. browser_hosts là chiều ngược lại: những host chủ đã nói là việc thường thì
mở không hỏi, và mọi thứ ngoài danh sách thì hỏi. Hai luật này là rào mềm như danh sách của
shell: chúng bắt lỗi vô ý và lời dụ dỗ vụng, không bắt được một trang cố tình đặt tên nút
sai.127.0.0.1 sau khi lời gọi đã qua. Luật ở đây chặt hơn fetch_url (mọi địa chỉ một tên
phân giải ra đều phải công khai) vì trình duyệt mang cookie.browser_* gần nhất trong chính cuộc đó.Từng tool, cửa sổ đăng nhập, tải tệp về, từng lời từ chối và các giới hạn đã biết ở tools.md. Màn hình của chính máy là một chuyện khác, ở mục dưới.
Có việc chỉ làm được trong một ứng dụng của máy: ứng dụng không có API hay trang web, hoặc chủ chỉ đăng nhập ở đó. Desktop trao cho agent chính màn hình chủ đang dùng. Khác với trình duyệt, không có hồ sơ riêng nào để cô lập: mỗi thao tác chạm vào máy thật, với mọi ứng dụng và mọi phiên đăng nhập của chủ. Những lựa chọn dưới đây đi từ điều đó.
desktop-helper/) chạy trong phiên đăng nhập của chủ; server chỉ gửi lệnh qua
một Unix socket và không tự chụp hay bấm gì. Các sản phẩm cùng loại cũng đặt việc chụp và gõ
phím trong một chương trình ký riêng như vậy. Quyền đi theo chữ ký, nên trình trợ giúp được ký
một lần rồi để yên: xây lại là phải cấp lại.computer chỉ đọc từ tệp của chính agent,
không thừa hưởng từ master, và danh sách tools: không đem tool nào tới. Ở mức máy, điều kiện
là trình trợ giúp đã được cài, đang chạy và có quyền, mà chỉ người ngồi trước máy làm được.
computer_apps là danh sách ứng dụng agent được đọc và thao tác; danh sách rỗng là chỉ nhìn.frame_id của khung hình mới nhất. Một ref chỉ có nghĩa
với đúng dàn ý nó nằm trong: nếu ref được đánh số chung cho mọi người, một lần đọc của hội thoại
khác sẽ làm ref cũ chỉ sang phần tử khác, và một lần bấm đã được duyệt có thể rơi vào nút khác.
Vì vậy ref gắn với dàn ý mà chính hội thoại ấy vừa xem, và bấm bằng ref phải nêu tên phần tử như
browser_act.computer_act cần duyệt như mọi tool đổi
trạng thái. Có những thao tác hỏi cả trong hội thoại tự chủ, vì thẻ duyệt không nói được chúng
sẽ làm gì: bấm theo toạ độ, Enter hay Space một mình (bấm thứ đang được chọn), phím ngoài nhóm
thường dùng, đưa một ứng dụng lên trước, chữ hay tên phần tử mang một từ dừng, và mọi lần bấm
đúp (trên một tệp hay chương trình, bấm đúp là mở nó, mà tên không nói được nó mở gì). Khi còn
tìm được ảnh của khung hình, thẻ duyệt hiện nó với điểm sắp bấm, đọc qua một route chỉ trả đúng
ảnh mà tool đã lưu.browser_* chặn ở từng request; còn chữ trang
viết là lời của người lạ. Một cú bấm hay phím gửi vào cửa sổ ấy vòng qua cả hai rào. Nên
computer_apps không nêu được trình duyệt và trình trợ giúp từ chối khi một trình duyệt ở
trước; trang web là việc của trình duyệt riêng của agent.sudo, tổ hợp phím đóng ứng dụng hay đổ thùng rác, chữ
cho ô bảo mật. Trình trợ giúp từ chối khoá máy, đăng xuất, buộc thoát, và mọi lần gõ, nhấn phím
hay đặt giá trị khi chế độ nhập bảo mật đang bật. Lời gọi chắc chắn bị từ chối thì tới thẳng lời
từ chối, không ai bị hỏi.
Những luật chữ này là rào mềm như danh sách của shell; rào cứng là ứng dụng nào được chạm và
thao tác nào không bao giờ được gửi.<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; tài liệu nói thẳng điều đó
(deployment-guide.md).Từng tool, từng lời từ chối và các giới hạn đã biết ở tools.md.
Chat hợp với câu trả lời đọc một lần. Một kế hoạch, một báo cáo hay một trang web mà người còn đọc lại và sửa tiếp thì cần một chỗ đứng riêng, nơi người và agent cùng sửa một tài liệu thay vì agent chép lại cả bài mỗi lần đổi một dòng. Đó là canvas; hướng dẫn đầy đủ ở canvas.md. Những quyết định định hình nó:
artifact_export hỏi duyệt. Duyệt tồn tại để chặn thay đổi không lùi lại được. Ghi
canvas thì lùi được bằng lịch sử, nên bốn tool ghi canvas không hỏi; xuất thì ghi đè một tệp
trong workspace, nên luôn hỏi. Bảy tool: canvas.md.POST /api/inbound chưa có đường nào mang canvas về cho người gọi, nên lượt từ
đó không tạo, sửa, viết lại hay nhập; nó vẫn liệt kê, đọc và xuất ra tệp, lần xuất vẫn chờ
duyệt. Agent được giao việc theo kênh của cuộc trò chuyện gốc của chuỗi.
canvas.md.html và mermaid là hai loại duy nhất
có trang để chạy. Trang đó 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. Đây là ngoại lệ có chủ đích so
với đường phục vụ tệp workspace ở mục trên, nơi không script nào được chạy. Canvas svg và
ảnh hiện như một bức hình nên không chạy gì. canvas.md.React + Vite, không có thư viện state. Hai reducer thuần ghim hợp đồng server từ cả hai phía: một trên luồng event của một cuộc trò chuyện, một trên luồng activity của rail. Mỗi vùng (một cuộc trò chuyện, danh sách, subscription activity, agent cùng job cùng stats) do một hook sở hữu, và một run kết thúc là tín hiệu làm mới các vùng còn lại. Cuộc trò chuyện đang mở còn tải lại khi một run mà tab này không stream (Telegram, một job, một quyết định trên máy khác) dừng chờ duyệt hay chạy tiếp sau quyết định, và một lần nữa khi luồng activity nối lại, vì trong lúc luồng đứt một run có thể đã chạy rồi xong; lịch sử duyệt đọc lại khi tập yêu cầu đang chờ đổi. Mọi chuỗi đều lấy từ một tệp chuỗi tiếng Việt.
Ô soạn không khoá khi agent đang bận: gõ và Enter vẫn gửi được, chỉ đổi chỗ tin đi tới. Tin
thường ra một chip “Đã xếp hàng, chạy sau lượt này” và không đụng luồng của lượt đang chạy —
web gửi nó bằng một POST riêng, chỉ đọc event queued từ đó, không chạm AbortController hay
reducer của lượt. Tin bắt đầu bằng /steer hay một lệnh kit ra chip “Sẽ chèn vào lượt đang
chạy”; khi server chèn nó (event steer), chip biến thành một tin người dùng ngay trong luồng,
và bước steer của run vẽ một hạt rỗng trên timeline để phân biệt “người nói” với “agent nói”.
Stop là lối duy nhất lấy chữ về: nó gọi server trước (POST …/stop, hạn ba giây) để hàng có cơ
hội bị xoá đúng thứ tự trước khi web abort luồng ở máy, rồi trả chữ các chip vào đầu ô soạn theo
thứ tự chúng được gửi. Chip là các chip xác nhận, không có nút huỷ riêng — quản lý hàng đợi
không phải phạm vi tính năng này. Một lượt drain (Telegram, một job, một tab khác) mà web không
dừng được từ đây báo cancelled: false; lúc đó Stop chỉ xoá chip và nói “đang chạy ở nơi khác
nên chưa dừng được từ đây” thay vì im lặng, còn khi Stop chỉ xoá chip mà không có gì đang chạy
thì không thông báo gì, vì nói “chạy ở nơi khác” lúc đó là sai. Hàng sống qua F5 vì chip của
thread lấy từ queued trong chi tiết cuộc trò chuyện mỗi lần loaded, không giữ riêng ở máy.
Bundle tách thành ba phần (react, vendor, mã ứng dụng), tên tệp mang hash nội dung;
server phục vụ /assets/* với Cache-Control: immutable và nén gzip mọi phản hồi trên
1 KB trừ luồng SSE, nên một bản phát hành chỉ đổi mã ứng dụng để trình duyệt giữ nguyên
hai phần kia, còn index.html luôn được hỏi lại.
Chính index.html cho trang biết server đã chạy bản build khác: trang so script entry có
hash (/assets/index-*.js) nó đang chạy với entry mà / đang phục vụ. Không so số phiên
bản, vì server hay được khởi động lại từ một working tree chưa bump phiên bản, và bump một
mình không đổi gì mà một lần tải lại mang về. Một app đã cài trên điện thoại có thể mở nhiều
ngày, nên trang nhìn lại khi được focus hay hiện lại (tối đa mỗi 30 giây, không bao giờ khi
tab đang ẩn) và sau mỗi lần luồng activity nối lại, vì một luồng đứt là dáng của một lần
khởi động lại khi nhìn từ trình duyệt. Entry khác thì thanh “Có bản mới” mời tải lại; một
lần nhìn hỏng không nói gì, vì server đang tắt không phải bản mới, còn Vite dev server
không có entry hash nên không bao giờ hiện thanh. Cài đặt ghi phiên bản của trang cạnh
phiên bản của server, và chỉ lấy tên đó từ một lần nhìn thấy server phục vụ đúng entry của
trang — trang tải trước một lần khởi động lại không được mang tên của bản nó không chạy.
Chỉ có một chat, với master: danh sách cuộc trò chuyện chứa các cuộc trò chuyện của master
và cuộc mới luôn được mở cho nó. Màn hình chào nói với tư cách master và nêu tên
đội; một chip trên header (Đội: N) mở tab đội trong rail, liệt kê mọi agent
(master trước, kèm huy hiệu mode, live, lịch và Telegram) và cài một mẫu đi kèm
bằng một cú bấm. Cuộc trò chuyện của agent được giao việc không nằm trong danh sách, nhưng mở được từ thẻ run
hoặc mục Cần bạn xử lý. Danh sách xếp các cuộc dưới Hôm nay / Hôm qua / Cũ hơn theo lịch
của chính người xem, không theo ngày UTC, và mỗi dòng nói nó đổi cách đây bao lâu. Từ tám
cuộc trở lên, sidebar thêm một ô tìm lọc tiêu đề tại chỗ trên trang, có debounce, gọi
GET /api/messages/search để tìm cả trong nội dung tin nhắn — không chỉ cuộc trò chuyện của
agent đang mở mà của cả đội, mỗi kết quả mang huy hiệu tên agent khi khác agent hiện tại và
bấm vào mở đúng cuộc trò chuyện đó. Tiêu đề không khớp gì nhưng nội dung có kết quả thì
không hiện “không có kết quả”, vì phần “Trong nội dung” bên dưới đã nói lên điều đó.
Màn hình chia theo việc thuộc về ai. Những gì cuộc trò chuyện bạn đang ở trong đó đang làm nằm
trong khung chat: một view activity của cuộc trò chuyện hiện các run của chính cuộc đó, từng step
kèm tham số và output của tool. Trên màn hình rộng, nó là một cột neo bên phải luồng
tin, luôn mở và giữ chỗ ngay cả trước run đầu tiên, nên chat không nhảy
khi việc bắt đầu. Hẹp hơn, nó gập thành một dải một dòng giữa luồng tin và ô soạn;
một trong hai được render, không bao giờ cả hai bị CSS ẩn. Những gì thuộc về cả
đội nằm trên một màn hình quản lý riêng (#/manage/<section>) thay vì trong một rail cạnh
luồng tin — để chúng cạnh nhau khiến việc của đội và việc của cuộc trò chuyện trông như
cùng một thứ. Nav của nó gom các mục thành theo dõi (activity, duyệt, chi phí), đội
(đội, tool, job, trí nhớ) và hệ thống (kết nối, cài đặt); trên điện thoại các nhóm dàn phẳng
thành một hàng cuộn ngang. Các mục của nó: activity (một mục Cần bạn xử lý cho các run
đã lỗi hoặc bị dừng, mỗi run một nút “Đã xem”; các run đang chạy; rồi lịch sử run đã lưu,
lọc theo agent ngay ở server và theo trạng thái, nguồn trong trang, đọc lùi thêm bằng
“Xem thêm”), duyệt (các yêu cầu đang chờ, quyết ngay tại chỗ — cho phép, từ chối hay trả lời
câu hỏi — kèm đồng hồ đếm tới hạn chót; bên dưới là lịch sử các yêu cầu đã quyết kèm kết
quả — đã duyệt, bị từ chối, hết hạn, đã trả lời), đội, tool, job (lịch đọc thành chữ cạnh
cron gốc, run kế/run trước kèm giờ đồng hồ, run trước kết thúc ra sao, nút chạy ngay, công
tắc tạm dừng/tiếp tục, nút sửa lịch mở trình sửa agent cho lịch viết tay, nút xoá cho lịch
tạo từ chat, và lịch sử run của job khi cần),
trí nhớ, chi phí theo agent, model và ngày — trong đó bảy ngày gần nhất và bảng theo model
được đọc thẳng từ nhật ký message với số token của nó, nên con số là thứ thực sự được tính
tiền chứ không phải ước lượng — kết nối, và cài đặt. Yêu cầu chờ duyệt và run hỏng nằm ở hai
trang vì việc làm với chúng khác nhau: yêu cầu thì quyết, lỗi thì đọc rồi cất đi. Mỗi mục
trên nav đếm đúng thứ của nó (activity: run đang chạy và lỗi chưa xem; duyệt: yêu cầu đang
chờ; job: job có run gần nhất hỏng; trí nhớ: đề xuất chờ), và mỗi trang chỉ sang trang kia
khi bên đó có việc, nên “không có gì cần xử lý” chỉ hiện khi đúng cho cả màn quản lý. Số yêu
cầu đang chờ còn đứng trước tiêu đề tab và lên badge của app đã cài, và nút Quản lý mở thẳng
trang duyệt khi có yêu cầu chờ.
Một run có thể mở riêng ở #/manage/activity/<run_id>: lấy theo id, nên link tới một run
mà danh sách chưa từng tải vẫn hoạt động, và tải lại trang vẫn ở đó. Phía trên mỗi timeline, một
dòng nói run đang làm gì lúc này — một câu và một số đếm (3/7 bước) thay vì
phần trăm, vì không có gì trong dữ liệu run nói còn bao nhiêu step nữa, nên
phần trăm sẽ là bịa. Run đã kết thúc thì thay vào đó nói nó kết thúc ra sao; một run đã xong
mà bảo đang suy nghĩ thì đọc như bị treo.
Trang riêng của run còn cho tải cả lượt chạy về, dạng JSON (cho máy, và làm nguồn dựng case
eval) hoặc Markdown (để đọc, dán vào issue): bản ghi của run, đúng những tin nhắn của run đó
trong cuộc trò chuyện, mọi lời gọi tool kèm tham số và kết quả, và bản ghi cuộc trò chuyện của
từng agent con mà run đã giao việc. Run mới ghi lại cuộc trò chuyện đang đứng ở tin nào khi nó
bắt đầu (after_seq), nên tin của nó là các tin sau mốc đó và trước mốc của run kế tiếp trong
cùng cuộc; run tiếp tục sau khi duyệt vẫn là run ấy nên giữ mốc cũ. Run có từ trước khi có mốc
thì được cắt theo giờ bắt đầu và kết thúc, và bản xuất ghi slice: "by_time" để người đọc biết.
Hai lượt chạy chồng nhau trong một cuộc thì ranh giới giữa chúng chỉ gần đúng. Agent con chỉ đi
theo khi run của nó mang nguồn delegate:<cuộc trò chuyện của run được xuất>: mã lời gọi do
provider cấp có thể trùng giữa hai cuộc, và tra theo mã trần sẽ kéo bản ghi của agent khác vào.
Agent con do một script mở (tools.delegate) cũng đi theo, cả khi script còn đang chờ nó lẫn
khi script đã xong: không kết quả nào nêu tên nó, nên nó được tìm theo mã của lời gọi
tool_script, phải được mở trong lúc run đang chạy, và tool_call_id của nó trong bản xuất là
<mã lời gọi tool_script>:<số>, số thứ tự của lời gọi trong script.
Bản xuất khác thứ người đã thấy trên màn hình: nó có mọi thứ tool đã chạm tới (đầu ra shell,
trang web, tệp trong workspace, dữ liệu một agent con mang về). Server che khoá theo cách tốt
nhất có thể, không bảo đảm: giá trị của mọi biến môi trường trong tiến trình server có tên
chứa KEY, TOKEN, SECRET, PASSWORD hay CREDENTIAL và dài từ 8 ký tự, cùng những
chuỗi có dạng khoá quen thuộc (sk-…, Bearer …, JWT), đều thành [đã che]. Bản ghi của run
giữ bản rút gọn chứ không giữ nguyên văn, nên một bí mật có thể bị cắt ngang; phần còn lại của
nó, từ 8 ký tự, cũng được che: phần đầu ở cuối chữ hay ngay trước dấu …, phần đuôi ở đầu chữ
hay ngay sau dấu …, kể cả dấu … nằm giữa lời của một bước hỏng (200 ký tự đầu, …, 400 ký
tự cuối). Một bí mật đến từ chỗ khác (người dùng gõ vào, một tệp hay trang web tool đọc được)
thì không bị che. Vì vậy đầu bản xuất và dưới hai link tải đều nhắc đọc lại trước khi dán ra
ngoài. Kết quả tool dài quá 2 000 ký tự bị cắt và ghi rõ dài bao nhiêu, trừ khi gọi với
full=1. Tệp đến dưới dạng attachment với Cache-Control: no-store, qua cùng hàng rào
Host/Origin như mọi đường khác.
Dưới mỗi tin người dùng đã lưu có một nút “Sửa và gửi lại từ đây”: bấm thì rẽ nhánh ngay tại đó
(ẩn khi thread đang bận, khi bubble còn xếp hàng hay khi hội thoại đang mở là con của delegate).
Bubble id số gọi API thẳng; bubble vừa gửi còn giữ id local-N (thread không nạp lại sau lượt của
chính nó) thì trước tiên đọc lại hội thoại một lần, khớp theo vị trí tin user tính từ cuối và chữ
của nó — không khớp thì báo lỗi chứ không đoán. Rẽ nhánh xong, web lưu chữ cũ làm bản nháp của
nhánh, làm mới danh sách rồi chuyển sang nhánh với ô soạn đã có sẵn chữ đó và đang focus. Header
của nhánh có thêm một dòng nhỏ “Rẽ nhánh từ …” dẫn về gốc (gốc không còn trong danh sách thì ghi
chung chung “cuộc trò chuyện gốc”), và tiêu đề mang hậu tố “(nhánh)” một lần, không cộng dồn qua
nhiều lần rẽ. Hội thoại gốc không đổi gì — không tin nào bị xoá hay viết lại.
Header chat là tiêu đề và ba pill: chi tiêu so với trần, tuỳ chọn, và số thành viên
đội. Pill mang dòng tóm tắt và mở một thẻ với chi tiết: thẻ chi tiêu
có thanh, phần còn lại, phần đã giao việc và chỗ nâng trần (thêm một bước, hoặc gõ trần mới
mà 0 là bỏ trần) — cũng chỗ đó nằm trong thông báo hết ngân sách, và ô soạn khoá tới khi
trần được nâng, vì server sẽ dừng lượt trước khi nó bắt đầu; thẻ tuỳ chọn giữ công tắc
autonomous, các skill tuỳ chọn dạng công tắc và các tool được cho phép luôn, mỗi cái kèm link
thu hồi, cùng nút xuất cả cuộc trò chuyện ra <tiêu đề>.md.
Thẻ đóng khi Escape (bắt trước phím tắt Escape của chính app, focus trả về pill)
hoặc bấm ra ngoài; bấm bên trong giữ thẻ mở. Thẻ ở mọi nơi dùng chung một từ vựng —
một hàng là icon, nhãn, gợi ý ⓘ, giá trị căn phải bằng chữ số đều (tabular; chỉ id và đường dẫn dạng code mới giữ font mono), một
thanh mỏng tuỳ chọn và một dòng phụ có màu — nên cột activity mở bằng một thẻ tóm tắt
(chi tiêu, step, run được giao việc, model) và cài đặt là một bộ thẻ tóm tắt chỉ đọc
dẫn tới mục nơi một thứ được thay đổi thay vì lặp lại danh sách của nó.
Thanh duyệt hiện hạn chót của yêu cầu đang chờ và một nút “luôn cho phép” cạnh duyệt/từ chối; khi dòng tóm tắt tham số phải cắt bớt, “Xem đầy đủ” mở toàn bộ tham số nguyên văn, ở thanh duyệt, ở thẻ tool và trong lịch sử duyệt. Màn chat và màn quản lý mỗi màn có một error boundary, nên một crash khi render hiện thẻ lỗi có nút tải lại thay vì trang trắng. Bên trong chat, thread và cột activity có boundary riêng, nên một phần vỡ không kéo sập phần còn lại; màn quản lý có một boundary cho mỗi trang, nên một mục vỡ vẫn để lại nav, và chọn trang khác là thoát khỏi lỗi. (Ý tưởng cho view run mượn từ view session của openhuman — không mượn code.)
Vài thứ chỉ thuộc về người đang nhìn màn hình này, nên sống trong localStorage của trình
duyệt chứ không ở server: mỗi cuộc trò chuyện đã được xem tới đâu (chấm chưa đọc), bản nháp
chưa gửi của từng cuộc, các chip lọc của lịch sử run, những lỗi đã bấm “Đã xem”, dải
activity đang mở hay gập, và việc có xem trước canvas agent đang viết hay không (công tắc
ở mục Cài đặt, lưu đúng một chữ off). Mọi lần đọc và ghi đi qua một helper lưu trữ cục
bộ bọc try/catch, vì trình duyệt có thể từ chối hẳn (cửa sổ riêng tư, chặn dữ liệu trang,
đầy quota): khi đó không gì được lưu và trang chạy tiếp trên bản trong bộ nhớ của nó cho
tới lần tải lại. Lần
dùng đầu coi mọi cuộc có từ trước là đã đọc, dấu đã xem của các tab cùng một trình duyệt
được gộp chứ không ghi đè nhau, và bản nháp của một cuộc bị xoá đi cùng cuộc đó. Trình duyệt
khác, hay máy khác, bắt đầu lại từ đầu — đó là cái giá của việc không bắt server nhớ một
điều chỉ đúng với một màn hình.
Danh sách cuộc trò chuyện xếp theo updated_at, và chấm chưa đọc so với chính mốc đó, nên
mốc chỉ được đẩy khi cuộc trò chuyện thật sự đổi: một tin nhắn, một lượt, một lần người dùng
sửa nó. Việc ghi sổ chạy nền — bản tóm tắt phiên, tiêu đề do model đặt, và chi phí của
chúng — ghi vào cuộc trò chuyện mà không đẩy mốc updated_at. Nếu không, một bản tóm tắt
viết xong sau khi người dùng đã rời đi sẽ đưa một cuộc cũ lên đầu danh sách và gắn chấm chưa
đọc cho thứ không ai viết thêm.
Về thị giác, mọi giá trị đi qua một bộ token trong web/src/styles/tokens.css: thang chữ,
khoảng cách, bo góc, bóng, bề mặt và các màu trạng thái, mỗi màu có bản sáng và tối (bản tối
nằm ở tokens-preferences.css). Chế độ
tối theo hệ điều hành, không có công tắc riêng. Font Inter đóng gói kèm bundle, gồm cả bộ
ký tự tiếng Việt, không tải từ CDN. Icon là một bộ nét SVG vẽ tay trong
components/ui/icon.tsx thay cho emoji, vì emoji mỗi nền tảng vẽ một kiểu và theme không
đổi được màu của nó. Mỗi agent có một avatar là chữ cái đầu trên nền màu riêng, băm từ id
nên ở đâu cũng cùng một màu. Logo trên sidebar cũng là favicon và icon khi cài app:
npm run icons vẽ các PNG từ web/public/favicon.svg. Thẻ Cần bạn xử lý xám khi cả
màn quản lý không còn gì chờ và chỉ chuyển màu cảnh báo khi có việc; trong lịch sử duyệt, yêu cầu hết hạn mang
nhãn xám, còn màu cảnh báo giữ cho yêu cầu bị từ chối và yêu cầu đang chờ. Nền tô của nút chính
và badge (--accent-fill) giữ màu xanh thương hiệu ở cả hai chế độ, vì bản xanh sáng hơn của
chế độ tối chỉ dành cho chữ và viền: chữ trắng trên nó không đạt 4.5:1.
Trên điện thoại, cuộc trò chuyện chiếm cả màn hình. Danh sách trượt vào từ nút menu, đóng
khi bấm Escape, chạm ra ngoài hoặc chọn một cuộc, và focus trả về nút đã mở nó. Khi mở,
drawer là modal: phần chat bên dưới inert, và Escape chỉ đóng drawer chứ không thu dải
activity nằm dưới. ⌘K mở drawer ngay ở ô tìm kiếm, vì ô đó nằm trong drawer đang đóng. Chấm
báo có việc chờ trên nút menu cũng nằm trong tên nút, để trình đọc màn hình nghe được. Nav của màn
quản lý thành một hàng pill, tự cuộn tới mục đang mở.
llm/): thứ stream ra một câu trả lời, nối vào nơi server dựng provider cho mỗi
agent.tools/): một spec cho model cộng một hàm chạy; đánh dấu cần duyệt khi
nó thay đổi trạng thái, và nối vào nơi server dựng bộ tool cho mỗi agent.mcp_servers của config.yaml và tên nó trong mcp: của agent;
không cần code. Xem tools.md.name (và tuỳ chọn always, description) trong MY_AGENT_HOME/skills
hoặc trong skills_dirs của một agent.MY_AGENT_HOME/agents/ với agent.yaml và các tệp persona.channels/): start, stop và deliver, dựng từ khối profile của master; giữ
secret dưới dạng tên biến môi trường trong profile.server/): một biến môi trường đã biết là một hàng trong catalogue (nhóm, secret hay URL, kiểm tra
trực tiếp tuỳ chọn); bất cứ thứ gì khác mà một skill đọc vẫn đặt được từ Kết nối dưới mục “Biến khác”.
Thay đổi được áp dụng ngay: đội được dựng lại từ môi trường mới và một thay đổi gây hỏng
(ví dụ xoá key duy nhất mà một tuyến cần) bị từ chối trước khi ghi bất cứ gì.
Một hàng rào cục bộ phủ mọi path: Host phải là IP, localhost hoặc tên trong
MY_AGENT_ALLOWED_HOSTS, và Origin phải khớp đúng host:port; 403 nêu tên host.