Chuyển đến nội dung

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_file thông thường; không có tool memory riêng.
  • SkillsMiddleware: BeforeRun quét các nguồn đã cấu hình tìm file SKILL.md, parse YAML frontmatter (name ≤64 ký tự, description ≤1024, allowed_tools tù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.md khô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 PromptCache trong 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 BeforeRun có 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.md củ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_load và agent.skills_index là con của agent.before_run, với hango.memory.sources, hango.memory.bytes, hango.skills.count, hango.skills.invalid.
  • Span event agent.memory_missing cho mỗi nguồn đã cấu hình mà không tồn tại, và agent.skill_invalid cho mỗi SKILL.md bị từ chối kèm path và lý do.
  • Attribute trên span chat: hango.memory.bytes và hango.skills.count để việc prompt phình ra truy được nguyên nhân; langfuse.observation.metadata.memory_bytes và ...skills_count để lọc trong Langfuse.
  • Metric: histogram hango.memory.load.duration, histogram hango.memory.bytes, gauge hango.skills.indexed, counter hango.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 config memory và skills là 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

  1. 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.
  2. Viết internal/agent/frontmatter.go: parse một khối --- ở đầu gồm các dòng key: value cộng các list đơn giản [a, b] vào schema Skill cố đị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.
  3. 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. SystemBlocks trả 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.
  4. Viết internal/agent/mwskills.go: BeforeRun glob {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 giao allowed_tools của từng skill với agent allowlist, và lưu chỉ mục. SystemBlocks chỉ render các dòng name — description (path) cộng một câu bảo model read_file path đó để lấy chỉ dẫn đầy đủ.
  5. Đặ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.
  6. 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.
  7. Thêm instrumentation từ phần ở trên.
  8. 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 owner ghi được /memories/AGENTS.md qua 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áo allowed_tools ngoài agent allowlist bị loại các tool đó và không gọi được chúng ; một SKILL.md khô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.md của tenant A.
  9. 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 trong path.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/memory với pgvector, channelmemory, một worker consolidation là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: MemoryMiddleware nạp các nguồn tại before_agent và nối chúng vào với một breakpoint cache_control của Anthropic; memory được sửa qua write_file thông thường trên route /memories/ của CompositeBackend; SkillsMiddleware với frontmatter SKILL.md (name ≤64, description ≤1024, allowed_tools), chỉ chèn chỉ mục vào prompt, nguồn cuối thắng, và skills_metadata khô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_load và agent.skills_index cho thấy những gì đã được nạp và mất bao lâu; các event agent.memory_missing và agent.skill_invalid giải thích “vì sao agent không biết skill của tôi”; hango.memory.bytes trên span chat giải thích việc prompt phình ra; hango.cache.read_ratio sụ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.go bao 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.md khô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.md của tenant A (test).
  • Trong Grafana, các span agent.memory_load và agent.skills_index xuất hiện dưới agent.before_run, và các series hango.memory.bytes/hango.skills.indexed tồn tại; hango.cache.read_ratio không sụp so với phase 10.

Đánh giá rủi ro

Rủi roKhả năng x Tác độngDấu hiệuPhản ứng định sẵn
Chèn memory làm vô hiệu prompt cacheCao x Caohango.cache.read_ratio sụp, chi phí input vọt lênSlot 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ổ contextCao x Trung bìnhhango.memory.bytes tăng theo từng runTrầ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 runTrung bình x CaoRun thất bại tại before_runChị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ạoTrung bình x Trung bìnhAgent ghi /skills/notes.txtpath.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 runTrung bình x Thấphango.memory.load.duration tăngMộ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ơnThấp x ThấpThèm tính năngNgoà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.