# Agent role và hồ sơ công cụ

> Each agent role loads only the tools it needs; measured before/after on coordinator and worker roles.

# Hồ sơ công cụ theo AgentRole (tool profiles)

**Ngày:** 2026-09-15 · **Trạng thái:** đã cài đặt, chờ khởi động lại Kazuo để có hiệu lực

## Vấn đề

Một role điều phối game nạp **71 công cụ / 182 KB mô tả mỗi lượt chat**, trong khi 12 phiên thật chỉ dùng
6 công cụ. Hai hậu quả: mỗi lượt im lặng khá lâu trước chữ đầu tiên, và điều phối viên luôn *có*
`Write`/`Edit`/`Agent` trong tay dù system prompt cấm nó tự sửa code.

## Cách hoạt động

Mỗi role chọn một **hồ sơ**. Hồ sơ suy ra hai thứ mà Agent SDK nhận:

| | Ý nghĩa |
|---|---|
| `tools` | Tập **công cụ gốc**. Theo type của SDK: *"the base set of available built-in tools"* — **không đụng tới công cụ MCP**, nên bộ plugin của role (`dispatch_agent`…) vẫn chạy dưới mọi hồ sơ. |
| `settingSources` | Nạp cấu hình từ nguồn nào trong `user` / `project` / `local`. Là **mảng ba nguồn, không phải công tắc** — nhờ vậy Thợ giữ được MCP Unity của chính project (`project`) trong khi bỏ MCP trình duyệt/ảnh toàn máy, danh mục skill và CLAUDE.md (đến từ `user`). |

### Ba hồ sơ

| Hồ sơ | Giữ | Bỏ | `settingSources` |
|---|---|---|---|
| **full** *(mặc định)* | tất cả — y như trước | — | không đổi |
| **orchestrator** | `Bash`, `PowerShell`, `Read`, `AskUserQuestion` + MCP theo `role.plugins` | `Write`, `Edit`, `Agent`, `Workflow`, trình duyệt, ảnh, skill, CLAUDE.md | `[]` |
| **worker** | `Read`, `Write`, `Edit`, `NotebookEdit`, `Glob`, `Grep`, `Bash`, `PowerShell`, `WebFetch`, `WebSearch`, `Monitor`, `TaskOutput`, `TaskStop`, `EnterPlanMode`, `ExitPlanMode` | `AskUserQuestion` (chạy không người trực → hỏi là treo tới hết giờ), `Agent` (một việc không được đẻ thêm việc), trình duyệt, ảnh, skill, CLAUDE.md | `['project']` — chính là nơi có MCP Unity |

**`full` là mặc định và không đổi gì.** Role không khai `toolProfile` (hoặc khai tên lạ) → trả về rỗng
và mọi thứ giữ nguyên. Đây là ràng buộc tương thích ngược cứng: 7 role sẵn có không được đổi hành vi.

### Tùy chỉnh nâng cao

`toolOverrides: { add: [], remove: [] }`, áp **sau** hồ sơ, `remove` thắng `add`. Chỉ có tác dụng khi
đã chọn một hồ sơ khác `full` — nên ô nhập trong giao diện chỉ hiện ra lúc đó, thay vì nhận vào rồi
âm thầm không làm gì.

## Nơi nó được diễn giải — một chỗ duy nhất

`connectAgentFor()` trong `plugins-library/ai-chat/main/chat-manager.ts` là chỗ **vốn đã** tra cứu
role (để lấy system prompt + bộ plugin), nên hồ sơ cũng được diễn giải tại đó. Vì vậy:

- `dispatch_agent` (worker **local**) truyền `attachedAgentId` (id role đã tra được) thay vì chỉ
  truyền prompt + plugins. Việc tra `attachedAgentId` → hồ sơ diễn ra **trên máy chạy worker** — với
  local, đó là cùng một máy đã tra role, nên luôn tìm thấy.
- Một phiên chat ghim role và một worker local được giao việc vào role đó **dùng chung một định nghĩa**.
- Phương án "dispatch tự tra hồ sơ rồi truyền `tools`" đã bị loại: nó tạo nguồn sự thật thứ hai.

### Worker **remote** (qua relay) — hồ sơ truyền BY VALUE, không truyền `attachedAgentId`

Sửa 2026-09-28: `dispatch_agent` nhánh remote trước đây quên truyền cả `attachedAgentId` lẫn hồ sơ —
worker ở máy khác chạy **không giới hạn tool nào** (bug, xem báo cáo debug ngày 2026-09-28). Không thể
fix bằng cách đơn giản là thêm `attachedAgentId` vào body remote như nhánh local, vì **role không đồng
bộ giữa máy** (xem `distributed-agent-orchestration.md`): id đó có thể không tồn tại trên máy đích
(im lặng rơi về "full" — đúng bug đang sửa) hoặc tệ hơn, trùng id với một role KHÁC không liên quan
trên máy đích, áp nhầm hồ sơ/systemPrompt của người ta.

Thay vào đó, máy A (nơi role thật sự được khai) tự tra `toolProfile`/`toolOverrides` của role rồi gửi
**by value** trong body của `POST /agent/dispatch` — cùng cách `systemPrompt`/`attachedPlugins` đã làm.
Máy B áp thẳng cặp giá trị này, không tra role nào theo id.

- `plugins-library/agent-role/main/index.ts`: `resolveRole()` trả thêm `toolProfile`/`toolOverrides`;
  `dispatch_agent` nhánh remote thêm `rbody.toolProfile`/`rbody.toolOverrides` (nhánh local giữ nguyên
  `attachedAgentId`, không đổi).
- `electron/dashboard-server.ts` `POST /agent/dispatch`: forward `body.toolProfile`/`body.toolOverrides`
  vào `opts.toolProfile`/`opts.toolOverrides` của `RunAgentOptions`.
- `plugins-library/ai-chat/main/chat-manager.ts`: `RunAgentOptions`/`TabState`/`openTab()` thêm
  `toolProfile`/`toolOverrides`. `connectAgentFor()` dùng `resolveToolProfileFor()` (mới, trong
  `tool-profiles.ts`) thay vì gọi `resolveToolProfile(agent.toolProfile, ...)` trực tiếp: nếu tab có
  `toolProfile` tường minh (remote dispatch) thì **thắng tuyệt đối**, không gộp với role tra được cục
  bộ — tránh đúng kiểu va chạm id nói trên. Tab local/UI bình thường không đặt `t.toolProfile` nên hành
  vi cũ (tra qua `agent`) giữ nguyên 100%.
- `plugins-library/ai-chat/main/tool-profiles.ts`: thêm `resolveToolProfileFor(explicit, role)` — hàm
  thuần, có test trong `tool-profiles.test.ts`.

**Không phải "role không tồn tại trên máy đích → cấp full quyền" nữa.** Máy đích giờ không tra role
theo id cho đường remote — nó chỉ áp đúng hồ sơ mà máy A đã tính sẵn. Nếu role không khai `toolProfile`
(dùng `full`), remote worker vẫn `full` như trước — không đổi hành vi cho các role hiện có.

## Files

- `plugins-library/ai-chat/main/tool-profiles.ts` — module **thuần**, định nghĩa hồ sơ + `resolveToolProfile()`
- `plugins-library/ai-chat/test/tool-profiles.test.ts` — 15 test
- `plugins-library/ai-chat/main/chat-manager.ts` — áp hồ sơ trong `connectAgentFor()`; `AgentBundle` thêm `toolProfile`/`toolOverrides`
- `plugins-library/agent-role/main/index.ts` — `resolveRole()` trả thêm `id`; `dispatch_agent` truyền `attachedAgentId`
- `plugins-library/agent-role/widget.html` + `js/widget.js` + `locales/{vi,en}.json5` — ô chọn hồ sơ trong trình sửa role

## Đo thật sau khi áp (2026-09-16)

Đo bằng cách cho mỗi role chạy thật một lần rồi tự liệt kê công cụ của nó — không ước lượng.

| Role | Trước | Sau | Ghi chú |
|---|---|---|---|
| role điều phối | 71 | **14** | 4 công cụ gốc + 10 MCP của chính nó |
| role thợ | 71 + MCP dự án | **88** | trong đó **54 là Unity MCP** — thợ vẫn gọi được đúng môi trường của nó |

Thợ nhiều hơn điều phối viên là **đúng thiết kế**: nó giữ `settingSources: ['project']` nên còn nguyên
Unity MCP và wiki arkon của chính dự án. Cái bị cắt là MCP toàn máy (trình duyệt 29, ảnh 3), danh mục
skill và CLAUDE.md — khoảng 32 công cụ cộng ~92 KB mô tả mỗi lượt.

Tiêu chí nghiệm thu 1-6 của báo cáo brainstorm đến đây đã đủ: role không khai hồ sơ thì không đổi;
điều phối viên giảm mạnh mà vẫn giao được việc đầu-cuối; thợ vẫn gọi được Unity MCP; `toolOverrides`
có test; module hồ sơ là thuần và có test; số trước/sau đã đo chứ không ước lượng.

## Giới hạn phải nói rõ

**Đây KHÔNG phải ranh giới an toàn.** Một worker vẫn còn `Bash` thì vẫn ghi được file dù đã cắt
`Write`. Cắt công cụ **thu hẹp** cơ hội làm sai và giảm chi phí mỗi lượt; cưỡng chế thật cần
PreToolUse hook (xem `tool-policy.ts`, `buildReadOnlyReviewPolicy`). Không được mô tả tính năng này
như một cơ chế bảo vệ.

Năm công cụ `Monitor`/`TaskOutput`/`TaskStop`/`EnterPlanMode`/`ExitPlanMode` được **giữ lại có chủ ý**
cho Thợ: mẫu dữ liệu sạch duy nhất là các việc chỉ-đọc, nên cắt chúng lúc này là đoán. Đo lại sau khi
có việc code thật.

## Hiệu lực

Thay đổi nằm trong `main/*.js` của ai-chat và agent-role — **không reload được** (reload `ai-chat`
sẽ giết mọi phiên Claude đang chạy). Hồ sơ chỉ có hiệu lực **từ lần khởi động Kazuo kế tiếp**.
