Memory dài hạn và skill
Tổng quan
Hai tính năng, một cơ chế: cả hai đều là quy ước đặt trên filesystem của phase 8. Không có tầng routing nào — /memories/ và /skills/ là các prefix được dành riêng trong backend Postgres duy nhất, được internal/fs/path.go thực thi, vì hango chỉ có đúng một backend và một router chỉ có một đích đến thì chẳng mang lại gì. Memory là context luôn bật: AGENTS.md và các nguồn khác được cấu hình sẽ được nạp một lần cho mỗi run và đóng góp dưới dạng các prompt block sau cache breakpoint. Skill dùng cơ chế tiết lộ dần (progressive disclosure): chỉ name, description và path đi vào prompt; phần thân được đọc theo yêu cầu bằng read_file.
Yêu cầu
Chức năng
- `/memories/` và `/skills/` là các prefix được dành riêng trong backend duy nhất dựa trên `agent_files` (namespace `{tenant_id, agent_id}`), được kiểm tra trong `internal/fs/path.go`. Không có `CompositeBackend`, không có bảng route, không viết lại path.
- `MemoryMiddleware` cài đặt đúng hai hook: `RunHook` (`BeforeRun` nạp từng nguồn đã cấu hình, mặc định `/memories/AGENTS.md`, bỏ các HTML comment, lưu nội dung vào private state có kiểu của nó) và `PromptContributor` (`SystemBlocks` trả nội dung đã nạp dưới dạng các block). Nó **không** cài `WrapModelCall`. Vì các system block được ghép theo thứ tự chain tại thời điểm gọi model và Memory nằm sau `PromptCache` trong stack, các block của nó rơi sau chỉ số breakpoint do cách thiết kế.
- File memory bị thiếu không phải lỗi: chúng bị bỏ qua và chỉ log một lần.
- Agent sửa memory bằng các tool
write_file/edit_filethông thường; không có tool memory riêng. SkillsMiddleware:BeforeRunquét các nguồn đã cấu hình tìm fileSKILL.md, parse YAML frontmatter (name≤64 ký tự,description≤1024,allowed_toolstùy chọn), và chỉ chèn vào system prompt một bảng chỉ mục gồm name, description và path. Nguồn đến sau ghi đè các skill trùng tên.- `allowed_tools` của một skill chỉ có thể **thu hẹp**: tập hiệu lực là `allowed_tools ∩ agent allowlist`. Một file skill — là nội dung agent ghi được và do đó có thể bị kẻ tấn công tác động — không bao giờ được phép cấp một tool mà agent chưa được phép gọi. Các tên nằm ngoài giao tập bị loại kèm một cảnh báo và một span event `agent.skill_tools_narrowed`.
- Việc ghi vào `/memories/` và `/skills/` theo quy tắc của phase 8: chỉ qua API cho `owner`/`admin` cho đến phase 15, sau đó là một quy tắc HITL interrupt mặc định.
- Frontmatter
SKILL.mdkhông hợp lệ bị bỏ qua kèm một cảnh báo nêu path và lý do; một skill hỏng không bao giờ làm run thất bại. - Metadata skill và nội dung memory không bao giờ lan ngược từ subagent về agent cha.
- Nguồn memory và skill được cấu hình theo từng agent trong
agents.config(memory: [...],skills: [...]) với mặc định cấp tenant lấy từ config.
Phi chức năng
- Memory phải nằm sau
PromptCachetrong stack để các block nó đóng góp rơi sau chỉ số breakpoint, nếu không mỗi turn sẽ phải trả đủ chi phí input. Test thứ tự khẳng định chỉ số breakpoint, không phải thứ tự trong slice. - Việc nạp trong
BeforeRuncó giới hạn: một trần tổng số byte trên mọi nguồn (HANGO_MEMORY_MAX_BYTES, mặc định 64KiB) và một trần số lượng skill (HANGO_SKILLS_MAX, mặc định 100). - Test cô lập chéo tenant cho path memory và skill (tenant B không đọc được
/memories/AGENTS.mdcủa tenant A).
Kiến trúc
// Red team: F14 — no CompositeBackend. Reserved prefixes are enforced in internal/fs/path.go
// (fs.PrefixMemories, fs.PrefixSkills, fs.IsReserved), over the single pg backend.
// internal/agent/mwmemory.go
type MemoryMiddleware struct {
Backend fs.Backend
Sources []string // default {"/memories/AGENTS.md"}
MaxBytes int
} // RunHook (load once) + PromptContributor (blocks land after the cache breakpoint index)
// Red team: F6 — deliberately NOT a ModelWrapper.
// internal/agent/mwskills.go
type Skill struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
AllowedTools []string `yaml:"allowed_tools"`
Path string `yaml:"-"`
}
type SkillsMiddleware struct {
Backend fs.Backend
Sources []string // default {"/skills/"}
Max int
} // RunHook (index scan) + PromptContributor (index only, never bodies)Không có bảng mới: memory và skill là các dòng trong agent_files dưới các prefix được dành riêng.
Instrumentation
- Span:
agent.memory_loadvàagent.skills_indexlà con củaagent.before_run, vớihango.memory.sources,hango.memory.bytes,hango.skills.count,hango.skills.invalid. - Span event
agent.memory_missingcho mỗi nguồn đã cấu hình mà không tồn tại, vàagent.skill_invalidcho mỗiSKILL.mdbị từ chối kèm path và lý do. - Attribute trên span
chat:hango.memory.bytesvàhango.skills.countđể việc prompt phình ra truy được nguyên nhân;langfuse.observation.metadata.memory_bytesvà...skills_countđể lọc trong Langfuse. - Metric: histogram
hango.memory.load.duration, histogramhango.memory.bytes, gaugehango.skills.indexed, counterhango.skills.invalid,hango.cache.read_ratiođược kiểm tra lại ở đây vì memory là thủ phạm có khả năng cao nhất làm vô hiệu cache. - Trường log: một dòng info cho mỗi run tóm tắt các nguồn memory đã nạp, tổng số byte và số skill đã lập chỉ mục.
Các file liên quan
Tạo mới
internal/agent/mwmemory.go,internal/agent/mwmemory_test.go.internal/agent/mwskills.go,internal/agent/mwskills_test.go.internal/agent/frontmatter.go— parser YAML frontmatter tối giản cho schema cố định nhỏ (không dùng thư viện YAML), cộng test của nó.docs/learning/11-memory-and-skills.md.
Sửa đổi
internal/agent/mwstack.go— điền slot Skills (đứng đầu, để tool và chỉ mục của nó có sẵn cho mọi thứ khác) và slot Memory (sau PromptCache); cập nhật test cố định thứ tự.internal/agent/subagent.go,internal/agent/childbuilder.go— subagent nạp nguồn memory và skill của riêng mình và không bao giờ kế thừa các chỉ mục của agent cha.internal/fs/path.go— các hằng số prefix dành riêng vàIsReservedđã có từ phase 8; phase này chỉ thêm các test kiểm tra path memory và skill hợp lệ đúng cách.internal/config/config.go—HANGO_MEMORY_DEFAULT_SOURCES,HANGO_MEMORY_MAX_BYTES,HANGO_SKILLS_DEFAULT_SOURCES,HANGO_SKILLS_MAX.internal/server/handlers_agents.go— kiểm tra các mục configmemoryvàskillslà path tuyệt đối nằm dưới các prefix dành riêng.docs/system-architecture.md,docs/learning/README.md.
Các bước triển khai
- Xác nhận các prefix dành riêng hoạt động đúng: thêm các case bảng vào `internal/fs/path_test.go` cho `/memories/AGENTS.md`, `/skills/x/SKILL.md`, một prefix gần giống (`/memorie/x`) và một nỗ lực traversal vào một prefix dành riêng. Phase này không viết thêm code filesystem mới.
- Viết
internal/agent/frontmatter.go: parse một khối---ở đầu gồm các dòngkey: valuecộng các list đơn giản[a, b]vào schemaSkillcố định; từ chối các key không biết bằng cảnh báo thay vì lỗi; áp dụng các trần độ dài. - Viết
internal/agent/mwmemory.go:BeforeRunđọc từng nguồn (bỏ qua file thiếu), bỏ các comment<!-- ... -->, áp dụng trần tổng số byte bằng cách cắt kèm một dấu hiệu, và lưu kết quả vào private state có kiểu.SystemBlockstrả nội dung dưới dạng các block với một header ngắn nêu path của từng nguồn để agent biết sửa ở đâu. Không cóWrapModelCall. - Viết
internal/agent/mwskills.go:BeforeRunglob{source}/**/SKILL.md, parse frontmatter, áp dụng ghi đè theo kiểu nguồn cuối thắng theo tên, giới hạn số lượng, lấy giaoallowed_toolscủa từng skill với agent allowlist, và lưu chỉ mục.SystemBlockschỉ render các dòngname — description (path)cộng một câu bảo modelread_filepath đó để lấy chỉ dẫn đầy đủ. - Đặt các middleware vào
DefaultStack: Skills đầu tiên, Memory sau PromptCache. Thêm một chú thích tại slot Memory nêu ràng buộc về việc vô hiệu cache, và mở rộng test thứ tự để khẳng định các block của Memory rơi ở chỉ số cao hơn cache breakpoint. - Cập nhật child builder để subagent nạp nguồn của riêng mình và bắt đầu với private state memory/skill rỗng.
- Thêm instrumentation từ phần ở trên.
- Test: nội dung memory xuất hiện trong các system block đã ghép ở một chỉ số sau cache breakpoint (khẳng định chỉ số); nguồn thiếu bị bỏ qua kèm một event; trần byte cắt kèm dấu hiệu; một
ownerghi được/memories/AGENTS.mdqua files API và run kế tiếp nạp nội dung mới, trong khi một lần ghi từ tool của agent bị từ chối (integration test) ; chỉ mục skill chỉ chứa name/description/path và không bao giờ chứa phần thân; một skill khai báoallowed_toolsngoài agent allowlist bị loại các tool đó và không gọi được chúng ; mộtSKILL.mdkhông hợp lệ bị bỏ qua trong khi các skill hợp lệ vẫn được nạp; ghi đè theo kiểu nguồn cuối thắng; test chéo tenant rằng tenant B không đọc được/memories/AGENTS.mdcủa tenant A. - Cập nhật docs và viết learning doc.
Dàn ý tài liệu học
docs/learning/11-memory-and-skills.md phải có:
- Khái niệm: memory và skill là các quy ước đặt trên một filesystem chứ không phải các hệ thống con mới; tiết lộ dần như một kỹ thuật tiết kiệm ngân sách context.
- Các lựa chọn thiết kế:
/memories/và/skills/là prefix dành riêng được kiểm tra trongpath.go, memory là context luôn bật, skill chỉ chèn chỉ mục, subagent nạp nguồn của riêng mình. - Ghi chú học tập về phương án đã bị loại: deepagents định tuyến các prefix này qua một `CompositeBackend` vì nó thực sự có nhiều loại backend (state, filesystem, store). hango chỉ có một backend Postgres, nên một router sẽ là một tầng gián tiếp chỉ có một đích — chỉ riêng quy tắc prefix đã mang cùng ý nghĩa. Nếu một loại backend thứ hai xuất hiện (backend đĩa thật hoặc object store), khi đó router mới là bước đi đúng, và đây là ghi chú nói điều đó.
- Ràng buộc về thứ tự: các block của memory phải rơi sau chỉ số cache breakpoint; chi phí trông ra sao khi vi phạm, và vì sao phép khẳng định đặt trên chỉ số.
- File skill là nội dung agent ghi được: vì sao `allowed_tools` chỉ được thu hẹp, không bao giờ được cấp thêm.
- Đánh đổi: memory luôn bật so với một tool retrieval so với embedding; skill dựa trên file so với plugin đã đăng ký; namespace memory theo từng agent so với theo từng tenant.
- So sánh với goclaw:
internal/memoryvới pgvector,channelmemory, một workerconsolidationlàm “dreaming” kiểu episodic/semantic, và một knowledge graph — tất cả đều là tính năng sản phẩm mà hango cố ý bỏ qua; skill của goclaw là slash command, không phải tài liệu tiết lộ dần. - So sánh với deepagents:
MemoryMiddlewarenạp các nguồn tạibefore_agentvà nối chúng vào với một breakpointcache_controlcủa Anthropic; memory được sửa quawrite_filethông thường trên route/memories/củaCompositeBackend;SkillsMiddlewarevới frontmatterSKILL.md(name≤64,description≤1024,allowed_tools), chỉ chèn chỉ mục vào prompt, nguồn cuối thắng, vàskills_metadatakhông bao giờ được subagent kế thừa. - Vì sao hango sao chép phần này gần như y nguyên, và chỗ nó siết chặt hơn (kiểm tra prefix dành riêng, trần byte và số lượng, chịu được skill không hợp lệ).
- Cách debug tầng này: các span
agent.memory_loadvàagent.skills_indexcho thấy những gì đã được nạp và mất bao lâu; các eventagent.memory_missingvàagent.skill_invalidgiải thích “vì sao agent không biết skill của tôi”;hango.memory.bytestrên spanchatgiải thích việc prompt phình ra;hango.cache.read_ratiosụp sau phase này chỉ thẳng vào thứ tự của memory. - Bài tập: ghi memory qua files API và quan sát nó trong prompt của run kế tiếp; thêm một skill và xem chỉ mục xuất hiện; làm hỏng frontmatter và đọc cảnh báo.
- Ngân sách thời gian: 7h cài đặt + 2h learning doc.
Tiêu chí thành công
-
go build ./... && go vet ./... && go test ./...đều qua. -
path_test.gobao phủ các prefix dành riêng, một prefix gần giống và một nỗ lực traversal vào một prefix dành riêng. - Các block memory xuất hiện trong các system block đã ghép ở một chỉ số lớn hơn hẳn chỉ số cache breakpoint, và phản ánh một file đã ghi qua files API trong một run trước (integration test).
- Một skill khai báo
allowed_tools: [dangerous_tool]nằm ngoài agent allowlist không thể làm tool đó gọi được; event thu hẹp được ghi lại. - Một nguồn memory bị thiếu tạo ra một span event và không có lỗi.
- Chỉ mục skill chỉ chứa name, description và path; không có phần thân skill nào xuất hiện trong bất kỳ prompt nào (test).
- Một
SKILL.mdkhông hợp lệ bị bỏ qua kèm cảnh báo trong khi các skill còn lại vẫn được nạp (test). - Tenant B không đọc được
/memories/AGENTS.mdcủa tenant A (test). - Trong Grafana, các span
agent.memory_loadvàagent.skills_indexxuất hiện dướiagent.before_run, và các serieshango.memory.bytes/hango.skills.indexedtồn tại;hango.cache.read_ratiokhông sụp so với phase 10.
Đánh giá rủi ro
| Rủi ro | Khả năng x Tác động | Dấu hiệu | Phản ứng định sẵn |
|---|---|---|---|
| Chèn memory làm vô hiệu prompt cache | Cao x Cao | hango.cache.read_ratio sụp, chi phí input vọt lên | Slot Memory được ghim sau PromptCache bằng một test thứ tự; metric tỉ lệ cache được kiểm tra như một tiêu chí thành công |
| Memory phình không giới hạn ăn hết cửa sổ context | Cao x Trung bình | hango.memory.bytes tăng theo từng run | Trần tổng số byte kèm dấu hiệu cắt; agent được dặn trong header của prompt giữ memory ngắn gọn |
| Một skill lỗi định dạng làm hỏng mọi run | Trung bình x Cao | Run thất bại tại before_run | Chịu lỗi theo từng skill: bỏ qua, cảnh báo, tiếp tục; được khẳng định bằng một test có một skill hỏng và một skill hợp lệ |
| Prefix dành riêng đụng độ với path do agent tạo | Trung bình x Trung bình | Agent ghi /skills/notes.txt | path.go nhận diện các prefix và filesystem middleware từ chối các lần agent ghi vào chúng bằng một kết quả lỗi có giải thích; prompt ghi rõ các prefix dành riêng và ý nghĩa của chúng |
| Các hook nạp-một-lần thêm độ trễ cho mọi run | Trung bình x Thấp | hango.memory.load.duration tăng | Một query có index cho mỗi prefix nguồn; metric là điều kiện để thêm cache theo từng agent nếu có lúc nó thành vấn đề |
| Memory pgvector của goclaw trông mạnh hơn | Thấp x Thấp | Thèm tính năng | Ngoài phạm vi: memory ngữ nghĩa là tính năng sản phẩm; danh sách việc tiếp theo ghi lại điều này |
Rollback
Không có migration nào cần hoàn tác. Gỡ các slot Memory và Skills khỏi DefaultStack khôi phục hành vi của phase 10; các file dưới /memories/ và /skills/ vẫn nằm trong agent_files và vẫn đọc được.