From 5fe27ae642a86858af32763f62e493b5383dcc80 Mon Sep 17 00:00:00 2001 From: Wyatt_K Date: Mon, 21 Sep 2026 17:52:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(cloud-agent):=20=E6=8A=80=E8=83=BD=20descr?= =?UTF-8?q?iption=20=E8=BF=9B=E5=85=A5=20Agent=20=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=20manifest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compileCloudAgentPolicies 的技能 manifest 此前只含 skillId/name/version/hash/entryPath/files,模型看不到每个已激活技能是干什么的,无法在多技能间路由、也无法向用户解释为何选它。本改动把 SkillItem.Description(技能市场公开元数据)注入 manifest,并以 500 runes 为上限截断(描述由技能作者提供,需防止异常长的描述撑爆提示词预算)。skill_read_file 渐进披露边界不变:instruction 与文件正文仍不进入系统提示。 --- .../internal/app/cloud_agent_contract_test.go | 16 ++++++++++++++-- backend/internal/app/cloud_agent_policy.go | 15 ++++++++++++++- backend/internal/app/cloud_agent_tools.go | 5 ++++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/backend/internal/app/cloud_agent_contract_test.go b/backend/internal/app/cloud_agent_contract_test.go index 51e22506c..dcf46105b 100644 --- a/backend/internal/app/cloud_agent_contract_test.go +++ b/backend/internal/app/cloud_agent_contract_test.go @@ -215,16 +215,28 @@ func TestCloudAgentAnnotationRenderFeedsControlledTransientReference(t *testing. } func TestCloudAgentPolicyPublishesSkillManifestWithoutInliningSkillBody(t *testing.T) { - skill := cloudAgentSkill{ID: "skill-1", Name: "任务技能", Version: "v1", Hash: agentProfileHash("skill"), Instruction: "PRIVATE_SKILL_BODY", Files: map[string]string{"references/a.md": "A"}} + skill := cloudAgentSkill{ID: "skill-1", Name: "任务技能", Description: "当用户要写短剧剧本时调用", Version: "v1", Hash: agentProfileHash("skill"), Instruction: "PRIVATE_SKILL_BODY", Files: map[string]string{"references/a.md": "A"}} text, _, err := compileCloudAgentPolicies(agentTestRequest(), []cloudAgentSkill{skill}, "", cloudAgentProfileSnapshot{Revision: agentProfileRevision(nil), Hash: agentProfileHash("")}) if err != nil { t.Fatal(err) } - if strings.Contains(text, skill.Instruction) || !strings.Contains(text, `"entryPath":"SKILL.md"`) || !strings.Contains(text, `"files":["SKILL.md","references/a.md"]`) { + if strings.Contains(text, skill.Instruction) || !strings.Contains(text, `"entryPath":"SKILL.md"`) || !strings.Contains(text, `"files":["SKILL.md","references/a.md"]`) || !strings.Contains(text, `"description":"当用户要写短剧剧本时调用"`) { t.Fatalf("compiled policy did not publish a safe on-demand skill manifest: %s", text) } } +func TestCloudAgentPolicyTruncatesOversizedSkillDescription(t *testing.T) { + long := strings.Repeat("描", 600) + skill := cloudAgentSkill{ID: "skill-2", Name: "长描述技能", Description: long, Version: "v1", Hash: agentProfileHash("skill2")} + text, _, err := compileCloudAgentPolicies(agentTestRequest(), []cloudAgentSkill{skill}, "", cloudAgentProfileSnapshot{Revision: agentProfileRevision(nil), Hash: agentProfileHash("")}) + if err != nil { + t.Fatal(err) + } + if strings.Contains(text, long) || !strings.Contains(text, strings.Repeat("描", 500)+"…") { + t.Fatalf("oversized skill description was not capped at 500 runes: %s", text) + } +} + func TestCloudAgentPolicyPublishesCapabilityRoutingGuide(t *testing.T) { text, _, err := compileCloudAgentPolicies(agentTestRequest(), nil, "", cloudAgentProfileSnapshot{Revision: agentProfileRevision(nil), Hash: agentProfileHash("")}) if err != nil { diff --git a/backend/internal/app/cloud_agent_policy.go b/backend/internal/app/cloud_agent_policy.go index 7feb41f75..f89ba8729 100644 --- a/backend/internal/app/cloud_agent_policy.go +++ b/backend/internal/app/cloud_agent_policy.go @@ -84,6 +84,19 @@ func cloudAgentCapabilityGuide() string { return b.String() } +// cloudAgentSkillManifestDescription bounds the system prompt: the description +// is author-supplied public metadata, so it is trimmed and capped before it +// enters the compiled policy. +func cloudAgentSkillManifestDescription(description string) string { + const maxRunes = 500 + trimmed := strings.TrimSpace(description) + if utf8.RuneCountInString(trimmed) <= maxRunes { + return trimmed + } + runes := []rune(trimmed) + return strings.TrimSpace(string(runes[:maxRunes])) + "…" +} + func compileCloudAgentPolicies(req CloudAgentRequest, skills []cloudAgentSkill, canvasSummary string, profile cloudAgentProfileSnapshot, anchors ...cloudAgentCreativeAnchor) (string, cloudAgentPolicySnapshot, error) { system, media, err := prompts.LoadAgentPolicies() if err != nil { @@ -121,7 +134,7 @@ func compileCloudAgentPolicies(req CloudAgentRequest, skills []cloudAgentSkill, } manifests := make([]map[string]any, 0, len(skills)) for _, skill := range skills { - manifests = append(manifests, map[string]any{"skillId": skill.ID, "name": skill.Name, "version": skill.Version, "hash": skill.Hash, "entryPath": cloudAgentSkillEntryPath, "files": cloudAgentSkillPaths(skill)}) + manifests = append(manifests, map[string]any{"skillId": skill.ID, "name": skill.Name, "description": cloudAgentSkillManifestDescription(skill.Description), "version": skill.Version, "hash": skill.Hash, "entryPath": cloudAgentSkillEntryPath, "files": cloudAgentSkillPaths(skill)}) } context["skills"] = manifests layers := make([]map[string]any, 0, len(profile.Layers)) diff --git a/backend/internal/app/cloud_agent_tools.go b/backend/internal/app/cloud_agent_tools.go index 742038745..5455c3915 100644 --- a/backend/internal/app/cloud_agent_tools.go +++ b/backend/internal/app/cloud_agent_tools.go @@ -22,6 +22,7 @@ import ( type cloudAgentSkill struct { ID string `json:"id"` Name string `json:"name"` + Description string `json:"description,omitempty"` Version string `json:"version"` Hash string `json:"hash"` Instruction string `json:"instruction,omitempty"` @@ -60,7 +61,9 @@ func (s *Service) cloudAgentSkills(userID string, ids []string) ([]cloudAgentSki } // Skill content is loaded only after the model explicitly calls // skill_read_file; keep the run context to stable metadata and paths. - snapshot := cloudAgentSkill{ID: id, Name: skill.SkillName, Version: skill.VersionID, Hash: skill.ContentHash, Files: map[string]string{cloudAgentSkillEntryPath: ""}} + // The description is public metadata (market listing) and lets the + // model route between activated skills without reading any body. + snapshot := cloudAgentSkill{ID: id, Name: skill.SkillName, Description: skill.Description, Version: skill.VersionID, Hash: skill.ContentHash, Files: map[string]string{cloudAgentSkillEntryPath: ""}} files, err := s.SkillPackageFiles(userID, id) if err != nil { return nil, err