返回 DeepSeek-Reasonix
index.go
根目录 / internal / skill / index.go
1 package skill
2
3 import (
4 "strings"
5
6 "reasonix/internal/textutil"
7 )
8
9 // IndexMaxChars caps the session-context skills catalog; bodies never enter it.
10 const IndexMaxChars = 4000
11
12 const missingDescPlaceholder = `(no description — frontmatter is missing a "description:" line; tell the user to add one)`
13
14 // indexHeader is the cache-stable invocation policy. The dynamic catalog is
15 // delivered independently in the latest host-generated session-context.
16 const indexHeader = "# Skills — playbooks you can invoke\n\n" +
17 "The latest host-generated `<session-context>` contains the skills catalog. Use a skill when the user names it or its guidance materially helps the task; keyword overlap alone is insufficient. Load only relevant references. Call `run_skill` with the bare name and concrete task in `arguments`, or use the dedicated tool when available. Inline skills return instructions; `[🧬 subagent]` skills execute in isolation and return a final answer. Skill instructions do not expand the user's authorization. The user can also invoke `/<name>`. Discover omitted skills with `use_capability` action=search."
18
19 const readOnlyIndexHeader = "# Skills — read-only playbooks you can invoke\n\n" +
20 "The latest host-generated `<session-context>` contains the current one-line catalog for this narrow read-only skill surface. Call `read_only_skill({ name: \"<skill-name>\", arguments: \"<task>\" })` — `name` is JUST the identifier, NOT the `[🧬 subagent]` tag. Inline skills are loaded into context. Skills tagged `[🧬 subagent]` run in an isolated ephemeral read-only subagent with only read-only research tools and safe foreground bash; no writes, installers, memory mutation, continuation/fork, background jobs, or writer-capable delegation are available. Read-only nested delegation may be available until max_subagent_depth is reached."
21
22 // InvocationPolicyBlock is the stable executor policy without catalog entries.
23 func InvocationPolicyBlock() string { return indexHeader }
24
25 // ReadOnlyInvocationPolicyBlock is the stable planner policy without catalog entries.
26 func ReadOnlyInvocationPolicyBlock() string { return readOnlyIndexHeader }
27
28 // CatalogBlock renders only dynamic names, descriptions, and run tags.
29 func CatalogBlock(skills []Skill) string { return catalogBlock(skills) }
30
31 // ReadOnlyCatalogBlock currently has the same entries as CatalogBlock; the
32 // planner-specific invocation semantics remain in ReadOnlyInvocationPolicyBlock.
33 func ReadOnlyCatalogBlock(skills []Skill) string { return catalogBlock(skills) }
34
35 // IndexBlock renders the system/tool-result skills listing without attaching it
36 // to a base prompt. Only names + descriptions (+ a subagent tag) are listed;
37 // bodies load on demand via run_skill.
38 func IndexBlock(skills []Skill) string {
39 return indexBlockWithHeader(indexHeader, skills)
40 }
41
42 // ReadOnlyIndexBlock renders the same listing with read_only_skill-specific
43 // invocation guidance for token-economy plan-mode connections.
44 func ReadOnlyIndexBlock(skills []Skill) string {
45 return indexBlockWithHeader(readOnlyIndexHeader, skills)
46 }
47
48 func indexBlockWithHeader(header string, skills []Skill) string {
49 catalog := catalogBlock(skills)
50 if catalog == "" {
51 return ""
52 }
53 return header + "\n\n" + catalog
54 }
55
56 func catalogBlock(skills []Skill) string {
57 if len(skills) == 0 {
58 return ""
59 }
60 visible := make([]Skill, 0, len(skills))
61 for _, sk := range skills {
62 // Manual-invocation skills (e.g. user-authored subagent profiles) stay
63 // invocable by name (/<name>, run_skill) but must never enter the
64 // session-context catalog the model scans for candidates on its own
65 // initiative.
66 if sk.Invocation == "manual" {
67 continue
68 }
69 visible = append(visible, sk)
70 }
71 if len(visible) == 0 {
72 return ""
73 }
74 return boundedCatalog(visible)
75 }
76
77 // ApplyIndex appends the skills index to basePrompt, or returns it unchanged
78 // when there are no skills. Only names + descriptions (+ a subagent tag) are
79 // listed; bodies load on demand via run_skill.
80 func ApplyIndex(basePrompt string, skills []Skill) string {
81 block := IndexBlock(skills)
82 if block == "" {
83 return basePrompt
84 }
85 return basePrompt + "\n\n" + block
86 }
87
88 // Keep the full identifier and run tag while sharing the description budget.
89 func indexLineWithLimit(sk Skill, descriptionLimit int) string {
90 desc := strings.TrimSpace(strings.ReplaceAll(sk.Description, "\n", " "))
91 if desc == "" {
92 desc = missingDescPlaceholder
93 }
94 tag := ""
95 if sk.RunAs == RunSubagent {
96 tag = " [🧬 subagent]"
97 }
98 max := min(descriptionLimit, 130-len([]rune(sk.Name))-len([]rune(tag)))
99 clipped := clipRunes(desc, max)
100 if clipped == "" {
101 return "- " + sk.Name + tag
102 }
103 return "- " + sk.Name + tag + " — " + clipped
104 }
105
106 // clipRunes preserves the historical name but clips by grapheme clusters so
107 // combined emoji and other user-visible characters stay intact.
108 func clipRunes(s string, max int) string {
109 return textutil.ClipGraphemes(s, max, "…")
110 }
111
111 lines GO