From 02df33803ac2a9876399c1606cfa2724658cf305 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Wed, 25 Mar 2026 15:17:59 +0800 Subject: [PATCH] feat: structured skills system with meta skill runtime injection Replace agent.skills TEXT field with structured skill/skill_file/agent_skill tables. Skills are workspace-level entities with supporting files, reusable across agents via many-to-many bindings. Backend: migration 008, sqlc queries, CRUD handler, agent-skill junction, structured skill loading in task context snapshot. Daemon: meta skill injection via runtime-native config (.claude/CLAUDE.md for Claude, AGENTS.md for Codex) so agents discover .agent_context/ skills through their native mechanism. Lean prompt without inlined skill content. Frontend: Skills management page, agent Skills tab picker, SDK methods, TypeScript types, workspace store integration. Also removes auto-creation of init issues when creating agents. Co-Authored-By: Claude Opus 4.6 --- .../(dashboard)/_components/app-sidebar.tsx | 2 + apps/web/app/(dashboard)/agents/page.tsx | 160 ++++-- apps/web/app/(dashboard)/skills/page.tsx | 538 ++++++++++++++++++ apps/web/features/workspace/store.ts | 21 +- apps/web/test/helpers.tsx | 2 +- packages/sdk/src/api-client.ts | 42 ++ packages/types/src/agent.ts | 48 +- packages/types/src/events.ts | 5 +- packages/types/src/index.ts | 5 + pnpm-lock.yaml | 9 - server/cmd/server/router.go | 16 + server/internal/daemon/daemon.go | 44 +- server/internal/daemon/daemon_test.go | 15 +- server/internal/daemon/execenv/context.go | 66 ++- server/internal/daemon/execenv/execenv.go | 23 +- .../internal/daemon/execenv/execenv_test.go | 156 ++++- .../internal/daemon/execenv/runtime_config.go | 72 +++ server/internal/daemon/prompt.go | 15 +- server/internal/daemon/types.go | 19 +- server/internal/handler/agent.go | 115 ++-- server/internal/handler/skill.go | 531 +++++++++++++++++ server/internal/service/task.go | 40 +- .../migrations/008_structured_skills.down.sql | 4 + .../migrations/008_structured_skills.up.sql | 41 ++ server/pkg/db/generated/agent.sql.go | 28 +- server/pkg/db/generated/models.go | 28 +- server/pkg/db/generated/skill.sql.go | 382 +++++++++++++ server/pkg/db/queries/agent.sql | 5 +- server/pkg/db/queries/skill.sql | 80 +++ 29 files changed, 2320 insertions(+), 192 deletions(-) create mode 100644 apps/web/app/(dashboard)/skills/page.tsx create mode 100644 server/internal/daemon/execenv/runtime_config.go create mode 100644 server/internal/handler/skill.go create mode 100644 server/migrations/008_structured_skills.down.sql create mode 100644 server/migrations/008_structured_skills.up.sql create mode 100644 server/pkg/db/generated/skill.sql.go create mode 100644 server/pkg/db/queries/skill.sql diff --git a/apps/web/app/(dashboard)/_components/app-sidebar.tsx b/apps/web/app/(dashboard)/_components/app-sidebar.tsx index b1ebd3b4..f79c5509 100644 --- a/apps/web/app/(dashboard)/_components/app-sidebar.tsx +++ b/apps/web/app/(dashboard)/_components/app-sidebar.tsx @@ -13,6 +13,7 @@ import { LogOut, Plus, Check, + Sparkles, } from "lucide-react"; import { MulticaIcon } from "@/components/multica-icon"; import { @@ -52,6 +53,7 @@ import { useWorkspaceStore } from "@/features/workspace"; const navItems = [ { href: "/inbox", label: "Inbox", icon: Inbox }, { href: "/agents", label: "Agents", icon: Bot }, + { href: "/skills", label: "Skills", icon: Sparkles }, { href: "/issues", label: "Issues", icon: ListTodo }, { href: "/knowledge-base", label: "Knowledge Base", icon: BookOpen }, ]; diff --git a/apps/web/app/(dashboard)/agents/page.tsx b/apps/web/app/(dashboard)/agents/page.tsx index 7e9c6ea4..9233133b 100644 --- a/apps/web/app/(dashboard)/agents/page.tsx +++ b/apps/web/app/(dashboard)/agents/page.tsx @@ -43,7 +43,6 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { api } from "@/shared/api"; import { useAuthStore } from "@/features/auth"; @@ -305,28 +304,40 @@ function AgentListItem({ } // --------------------------------------------------------------------------- -// Skills Tab +// Skills Tab (picker — skills are managed on /skills page) // --------------------------------------------------------------------------- function SkillsTab({ agent, - onSave, }: { agent: Agent; - onSave: (skills: string) => Promise; }) { - const [skills, setSkills] = useState(agent.skills); + const workspaceSkills = useWorkspaceStore((s) => s.skills); + const refreshAgents = useWorkspaceStore((s) => s.refreshAgents); const [saving, setSaving] = useState(false); - const isDirty = skills !== agent.skills; + const [showPicker, setShowPicker] = useState(false); - useEffect(() => { - setSkills(agent.skills); - }, [agent.id, agent.skills]); + const agentSkillIds = new Set(agent.skills.map((s) => s.id)); + const availableSkills = workspaceSkills.filter((s) => !agentSkillIds.has(s.id)); - const handleSave = async () => { + const handleAdd = async (skillId: string) => { setSaving(true); try { - await onSave(skills); + const newIds = [...agent.skills.map((s) => s.id), skillId]; + await api.setAgentSkills(agent.id, { skill_ids: newIds }); + await refreshAgents(); + } finally { + setSaving(false); + setShowPicker(false); + } + }; + + const handleRemove = async (skillId: string) => { + setSaving(true); + try { + const newIds = agent.skills.filter((s) => s.id !== skillId).map((s) => s.id); + await api.setAgentSkills(agent.id, { skill_ids: newIds }); + await refreshAgents(); } finally { setSaving(false); } @@ -338,26 +349,114 @@ function SkillsTab({

Skills

- Define what this agent does and how it should accomplish tasks. Supports Markdown. + Reusable skills assigned to this agent. Manage skills on the Skills page.

- {isDirty && ( - - )} + -