@@ -3,9 +3,11 @@ package runtime
33import (
44 "context"
55 "errors"
6+ "sort"
67 "strings"
78 "time"
89
10+ providertypes "neo-code/internal/provider/types"
911 agentsession "neo-code/internal/session"
1012 "neo-code/internal/skills"
1113)
@@ -19,6 +21,12 @@ type SessionSkillState struct {
1921 Descriptor * skills.Descriptor
2022}
2123
24+ // AvailableSkillState 描述当前可见 skill 的元信息及其在会话中的激活状态。
25+ type AvailableSkillState struct {
26+ Descriptor skills.Descriptor
27+ Active bool
28+ }
29+
2230// ActivateSessionSkill 在 session 级激活一个已注册的 skill。
2331func (s * Service ) ActivateSessionSkill (ctx context.Context , sessionID string , skillID string ) error {
2432 if err := ctx .Err (); err != nil {
@@ -113,6 +121,56 @@ func (s *Service) ListSessionSkills(ctx context.Context, sessionID string) ([]Se
113121 return states , nil
114122}
115123
124+ // ListAvailableSkills 返回当前 registry 中对会话可见的技能列表,并标记激活状态。
125+ func (s * Service ) ListAvailableSkills (ctx context.Context , sessionID string ) ([]AvailableSkillState , error ) {
126+ if err := ctx .Err (); err != nil {
127+ return nil , err
128+ }
129+ if s .skillsRegistry == nil {
130+ return nil , errSkillsRegistryUnavailable
131+ }
132+
133+ normalizedSessionID := strings .TrimSpace (sessionID )
134+ workspace := ""
135+ activeSet := map [string ]struct {}{}
136+ if normalizedSessionID != "" {
137+ session , err := s .sessionStore .LoadSession (ctx , normalizedSessionID )
138+ if err != nil {
139+ return nil , err
140+ }
141+ activeSet = skillSetFromIDs (session .ActiveSkillIDs ())
142+ if s .configManager != nil {
143+ workspace = agentsession .EffectiveWorkdir (session .Workdir , s .configManager .Get ().Workdir )
144+ } else {
145+ workspace = strings .TrimSpace (session .Workdir )
146+ }
147+ } else if s .configManager != nil {
148+ workspace = strings .TrimSpace (s .configManager .Get ().Workdir )
149+ }
150+
151+ descriptors , err := s .skillsRegistry .List (ctx , skills.ListInput {Workspace : workspace })
152+ if err != nil {
153+ return nil , err
154+ }
155+ if len (descriptors ) == 0 {
156+ return nil , nil
157+ }
158+
159+ states := make ([]AvailableSkillState , 0 , len (descriptors ))
160+ for _ , descriptor := range descriptors {
161+ key := normalizeRuntimeSkillID (descriptor .ID )
162+ _ , active := activeSet [key ]
163+ states = append (states , AvailableSkillState {
164+ Descriptor : descriptor ,
165+ Active : active ,
166+ })
167+ }
168+ sort .Slice (states , func (i , j int ) bool {
169+ return normalizeRuntimeSkillID (states [i ].Descriptor .ID ) < normalizeRuntimeSkillID (states [j ].Descriptor .ID )
170+ })
171+ return states , nil
172+ }
173+
116174// resolveActiveSkills 解析当前 session 激活的 skills,并对缺失项做事件降级。
117175func (s * Service ) resolveActiveSkills (ctx context.Context , state * runState ) ([]skills.Skill , error ) {
118176 if err := ctx .Err (); err != nil {
@@ -151,6 +209,42 @@ func (s *Service) resolveActiveSkills(ctx context.Context, state *runState) ([]s
151209 return resolved , nil
152210}
153211
212+ // prioritizeToolSpecsBySkillHints 按激活 skill 的 tool_hints 调整工具顺序,仅影响提示优先级。
213+ func prioritizeToolSpecsBySkillHints (
214+ specs []providertypes.ToolSpec ,
215+ activeSkills []skills.Skill ,
216+ ) []providertypes.ToolSpec {
217+ if len (specs ) == 0 {
218+ return nil
219+ }
220+ hints := collectSkillToolHints (activeSkills )
221+ if len (hints ) == 0 {
222+ return append ([]providertypes.ToolSpec (nil ), specs ... )
223+ }
224+
225+ rank := make (map [string ]int , len (hints ))
226+ for idx , hint := range hints {
227+ rank [hint ] = idx
228+ }
229+ prioritized := append ([]providertypes.ToolSpec (nil ), specs ... )
230+ sort .SliceStable (prioritized , func (i , j int ) bool {
231+ leftRank , leftHit := rank [normalizeRuntimeSkillID (prioritized [i ].Name )]
232+ rightRank , rightHit := rank [normalizeRuntimeSkillID (prioritized [j ].Name )]
233+ switch {
234+ case leftHit && rightHit :
235+ return leftRank < rightRank
236+ case leftHit :
237+ return true
238+ case rightHit :
239+ return false
240+ default :
241+ // 未命中的工具保持原有相对顺序,避免 hint 影响无关工具排序。
242+ return false
243+ }
244+ })
245+ return prioritized
246+ }
247+
154248// emitSkillMissingOnce 在同一次 run 内只上报一次指定 skill 的缺失事件,避免重复噪音。
155249func (s * Service ) emitSkillMissingOnce (ctx context.Context , state * runState , skillID string ) {
156250 if state == nil {
@@ -163,6 +257,45 @@ func (s *Service) emitSkillMissingOnce(ctx context.Context, state *runState, ski
163257 _ = s .emitRunScoped (ctx , EventSkillMissing , state , SessionSkillEventPayload {SkillID : skillID })
164258}
165259
260+ // collectSkillToolHints 收集并规范化激活 skills 中的 tool_hints,用于工具排序提示。
261+ func collectSkillToolHints (activeSkills []skills.Skill ) []string {
262+ if len (activeSkills ) == 0 {
263+ return nil
264+ }
265+ out := make ([]string , 0 , len (activeSkills ))
266+ seen := make (map [string ]struct {}, len (activeSkills ))
267+ for _ , skill := range activeSkills {
268+ for _ , hint := range skill .Content .ToolHints {
269+ normalized := normalizeRuntimeSkillID (hint )
270+ if normalized == "" {
271+ continue
272+ }
273+ if _ , ok := seen [normalized ]; ok {
274+ continue
275+ }
276+ seen [normalized ] = struct {}{}
277+ out = append (out , normalized )
278+ }
279+ }
280+ return out
281+ }
282+
283+ // skillSetFromIDs 将技能 ID 列表转换为规范化集合,便于快速判断激活状态。
284+ func skillSetFromIDs (ids []string ) map [string ]struct {} {
285+ if len (ids ) == 0 {
286+ return map [string ]struct {}{}
287+ }
288+ set := make (map [string ]struct {}, len (ids ))
289+ for _ , id := range ids {
290+ normalized := normalizeRuntimeSkillID (id )
291+ if normalized == "" {
292+ continue
293+ }
294+ set [normalized ] = struct {}{}
295+ }
296+ return set
297+ }
298+
166299// mutateSessionSkills 串行修改 session 的激活 skills,并在发生变化时立即持久化。
167300func (s * Service ) mutateSessionSkills (
168301 ctx context.Context ,
0 commit comments