mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
feat(skills): introduce built-in agent skills (WIP)
Inject platform-authored, version-bundled skills into every agent on top of its workspace-bound skills, so agents learn how to operate Multica correctly without users needing to know the internals or agents needing to read source. Mechanism: skills are embedded into the server binary and appended to the agent payload at task-claim time (handler/daemon.go), reusing the existing SkillData wire + daemon-side writeSkillFiles. The daemon needs no changes, and because it travels over an existing wire field, older daemons pick the skills up the moment the server ships. First skill: multica-mentioning — how to build a working @mention (look up the UUID, match type to id source, know what each mention type triggers). WIP: injection mechanism + first skill only; more skills to follow in dependency order (skill -> agent -> squad). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1097,7 +1097,11 @@ func (h *Handler) ClaimTaskByRuntime(w http.ResponseWriter, r *http.Request) {
|
||||
// Build response with fresh agent data (name + skills + custom_env + custom_args).
|
||||
resp := taskToResponse(*task)
|
||||
if agent, err := h.Queries.GetAgent(r.Context(), task.AgentID); err == nil {
|
||||
// Workspace-bound skills first, then platform built-in skills. Built-in
|
||||
// names carry a "multica-" prefix so their on-disk slugs never collide
|
||||
// with a user-authored workspace skill (see writeSkillFiles).
|
||||
skills := h.TaskService.LoadAgentSkills(r.Context(), task.AgentID)
|
||||
skills = append(skills, h.TaskService.BuiltinSkills()...)
|
||||
var customEnv map[string]string
|
||||
if agent.CustomEnv != nil {
|
||||
if err := json.Unmarshal(agent.CustomEnv, &customEnv); err != nil {
|
||||
|
||||
72
server/internal/service/builtin_skills.go
Normal file
72
server/internal/service/builtin_skills.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed builtin_skills
|
||||
var builtinSkillsFS embed.FS
|
||||
|
||||
const builtinSkillsRoot = "builtin_skills"
|
||||
|
||||
// BuiltinSkills returns the platform's built-in skills, embedded at compile
|
||||
// time. Every agent receives these on top of its workspace-bound skills, so
|
||||
// they teach platform-wide "how to" workflows (e.g. mentioning) that the
|
||||
// runtime brief intentionally leaves to skills.
|
||||
//
|
||||
// Layout: builtin_skills/<name>/SKILL.md plus optional supporting files. The
|
||||
// <name> directory carries a "multica-" prefix so its on-disk slug can never
|
||||
// collide with a workspace skill a user authored (see writeSkillFiles, which
|
||||
// derives the skill directory from AgentSkillData.Name).
|
||||
func (s *TaskService) BuiltinSkills() []AgentSkillData {
|
||||
return loadBuiltinSkills()
|
||||
}
|
||||
|
||||
func loadBuiltinSkills() []AgentSkillData {
|
||||
entries, err := fs.ReadDir(builtinSkillsFS, builtinSkillsRoot)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var skills []AgentSkillData
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
if skill, ok := loadBuiltinSkill(entry.Name()); ok {
|
||||
skills = append(skills, skill)
|
||||
}
|
||||
}
|
||||
return skills
|
||||
}
|
||||
|
||||
func loadBuiltinSkill(name string) (AgentSkillData, bool) {
|
||||
dir := path.Join(builtinSkillsRoot, name)
|
||||
content, err := fs.ReadFile(builtinSkillsFS, path.Join(dir, "SKILL.md"))
|
||||
if err != nil {
|
||||
// A skill directory without a SKILL.md is malformed — skip it rather
|
||||
// than ship an empty skill.
|
||||
return AgentSkillData{}, false
|
||||
}
|
||||
skill := AgentSkillData{Name: name, Content: string(content)}
|
||||
// Any other file in the directory becomes a supporting file, preserving
|
||||
// its relative path so subdirectories (e.g. rules/styling.md) survive.
|
||||
_ = fs.WalkDir(builtinSkillsFS, dir, func(p string, d fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil || d.IsDir() {
|
||||
return walkErr
|
||||
}
|
||||
rel := strings.TrimPrefix(p, dir+"/")
|
||||
if rel == "SKILL.md" {
|
||||
return nil
|
||||
}
|
||||
data, readErr := fs.ReadFile(builtinSkillsFS, p)
|
||||
if readErr != nil {
|
||||
return nil
|
||||
}
|
||||
skill.Files = append(skill.Files, AgentSkillFileData{Path: rel, Content: string(data)})
|
||||
return nil
|
||||
})
|
||||
return skill, true
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: multica-mentioning
|
||||
description: Use when writing an issue comment that needs to @mention someone — notify a person, trigger another agent, or hand work to a squad. Covers how to build a mention link that actually fires and what each mention type does.
|
||||
---
|
||||
|
||||
# Mentioning & Delegating
|
||||
|
||||
This skill covers HOW to build a mention that works. WHETHER to mention at all
|
||||
— loop avoidance, staying silent on acknowledgements — is already in your
|
||||
runtime brief's Mentions section; follow that and do not repeat it here.
|
||||
|
||||
## The one rule that breaks mentions
|
||||
|
||||
A mention link needs a REAL UUID. Writing `[@Alice](mention://member/Alice)`
|
||||
does NOTHING: a name is not a UUID, so the link silently fails — no
|
||||
notification, no trigger, no error. Always look up the UUID first.
|
||||
|
||||
## Step 1 — look up the UUID with `--output json`
|
||||
|
||||
- a person → `multica workspace member list --output json` → use `user_id`
|
||||
- an agent → `multica agent list --output json` → use `id`
|
||||
- a squad → `multica squad list --output json` → use `id`
|
||||
|
||||
Match by display name. If the name is ambiguous or absent, do not guess —
|
||||
say so in your comment instead of emitting a broken link.
|
||||
|
||||
## Step 2 — build the link; type and id source MUST match
|
||||
|
||||
Format: `[@Name](mention://<type>/<uuid>)`
|
||||
|
||||
| To… | type | uuid from | What it triggers |
|
||||
| -------------------- | -------- | -------------- | ----------------------------------------- |
|
||||
| notify a person | `member` | member.user_id | sends them a notification (no run) |
|
||||
| make an agent work | `agent` | agent.id | enqueues a run for that agent |
|
||||
| hand work to a squad | `squad` | squad.id | enqueues the squad LEADER, who delegates |
|
||||
| reference an issue | `issue` | issue.id | nothing — a plain link, always safe |
|
||||
|
||||
Using the wrong `type` for an id points at the wrong entity or fails silently.
|
||||
|
||||
**`@all` is the exception** — it uses the literal `all`, never a UUID:
|
||||
`[@all](mention://all/all)`. It broadcasts to everyone on the issue; it does
|
||||
NOT make any specific agent run, and it also suppresses the assignee's
|
||||
automatic on-comment trigger. Use it to announce, not to request work.
|
||||
|
||||
## What does NOT happen (so the result doesn't surprise you)
|
||||
|
||||
- A wrong/missing UUID, or a bare `@name`, silently does nothing.
|
||||
- `@member` never makes a person "run" — it only notifies them.
|
||||
- Even a correct mention may not fire if the target agent already has a
|
||||
pending task on this issue, is archived, or is private and you cannot
|
||||
access it. That is expected — do not retry in a loop.
|
||||
|
||||
## Incorrect → Correct
|
||||
|
||||
Incorrect: `@alice please review`
|
||||
→ plain text, no link, nobody is notified.
|
||||
|
||||
Incorrect: `[@Alice](mention://member/Alice) please review`
|
||||
→ "Alice" is not a UUID, the link is silently dead.
|
||||
|
||||
Correct:
|
||||
1. `multica workspace member list --output json` → Alice's user_id = 7f3a…
|
||||
2. `[@Alice](mention://member/7f3a…) please review`
|
||||
|
||||
## Source of truth
|
||||
|
||||
These behaviors are hard-coded in the Multica backend. If a mention does not
|
||||
behave as described, check the source rather than guessing:
|
||||
|
||||
- `server/internal/util/mention.go:16` — the mention regex. The id must be a
|
||||
hex UUID (or the literal `all`); a name silently fails to parse.
|
||||
- `server/internal/handler/comment.go:884` — `enqueueMentionedAgentTasks`:
|
||||
how `@agent` enqueues a run and `@squad` enqueues the leader, plus the
|
||||
guards (already-pending dedup, archived, private) that make a valid mention
|
||||
no-op.
|
||||
- `server/internal/handler/comment.go:768` — `@all` is a broadcast that
|
||||
suppresses the assignee's auto-trigger.
|
||||
Reference in New Issue
Block a user