@deepseek-ai/dsh-tool-skill 0.0.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +169 -0
- package/README.zh.md +169 -0
- package/lib/index.js +375 -0
- package/lib/invariant.js +23 -0
- package/lib/types/index.d.ts +46 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/skill/tool-skill/README.md
|
|
5
|
+
README.md: 704eb7eb1f611c20f76ce79190326296ff63da42
|
|
6
|
+
README.zh.md: 09fb5b954ea595b962f7670ce09db1ae22dce29c
|
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-tool-skill
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
The model-facing skill catalog and `skill` tool.
|
|
6
|
+
|
|
7
|
+
Requires `ctx.agents`, `ctx.tools`, and `ctx.skills` (`inject: ['agents', 'tools', 'skills']`).
|
|
8
|
+
|
|
9
|
+
## Catalog lifecycle
|
|
10
|
+
|
|
11
|
+
At every eligible `agent/pre-step`, the plugin calls `ctx.skills.snapshot()` for the calling session's cwd, forwards the pre-step abort signal to discovery, applies exact `skill` tool visibility, and renders the ordered `name` and `description` entries. When no prior catalog exists and that view is non-empty, it adds an initial durable user-role `<system-reminder>` to a downstream `enter` decision. Catalog messages contain only those summaries; skill bodies, paths, sources, providers, and `whenToUse` hints remain outside the catalog.
|
|
12
|
+
|
|
13
|
+
Every catalog message carries the `skill-catalog` source: a `catalog`-form context whose `entries` record exactly the `name` and `description` pairs it published, plus `update` on a replacement. The digest covers those durable entries, not the rendered prose, so the surrounding `<system-reminder>` framing cannot decide whether a republish is needed and consumers never re-parse the `<available_skills>` block. The plugin scans durable session events backwards without copying them and derives the comparison baseline from the newest visible `skill-catalog` message it can read; unreadable and foreign records are skipped. When the digest changes, the downstream `enter` decision receives a durable user-role message containing the complete replacement catalog; an empty replacement explicitly retires earlier names. If no catalog remains visible but a recognizable historical catalog exists, compaction hid it and the next complete observation re-establishes the current catalog. An incomplete provider snapshot emits nothing and preserves the last-good model view for retry at the next pre-step. If no prior catalog exists and the current view is empty, no tombstone is necessary.
|
|
14
|
+
|
|
15
|
+
The catalog is omitted when no model-invocable skills are initially available, and also when that agent's tool view restricts away the shipped `skill` tool or resolves a same-name scoped shadow instead. Identity is compared against the definition this plugin registered rather than a lookup of its own name, so the plugin works mounted globally or inside one agent's composition, where `register()` files into that agent's layer alone. Visibility changes participate in the digest, keeping prompt guidance, model-visible schema, and executable dispatch aligned.
|
|
16
|
+
|
|
17
|
+
`catalogDescriptionMaxLength` controls normalized catalog descriptions; rendering XML-escapes them. Its default is `500` and values must be integers of at least `3`, which reserves room for a truncation ellipsis. The [skill catalog hot-refresh Agent Note](../../../.agents/notes/implemented/feature/2026-07-27-skill-catalog-hot-refresh.md) owns the durable initial catalog and replacement lifecycle.
|
|
18
|
+
|
|
19
|
+
## Tool: `skill`
|
|
20
|
+
|
|
21
|
+
| Arg | Type | Notes |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `name` | string (required) | Exact kebab-case skill name from the available skills listing. |
|
|
24
|
+
|
|
25
|
+
Execution uses the calling agent's `session.header.cwd` so workspace-sensitive providers resolve the winning skill. A successful call returns canonical `{ name, provider, resourceBase?, content }`, excluding catalog ranking and provider-internal machinery; its Native renderer produces one text result containing `<skill_content name="...">`, `<skill_resources>`, and `<skill_instructions>`.
|
|
26
|
+
|
|
27
|
+
Resource guidance resolves only paths or URLs explicitly referenced by the instructions against `resourceBase`; scripts, references, and assets load on demand, and the result does not enumerate a skill directory. Local providers may supply a directory, while remote or embedded providers may supply a URL or opaque loading guidance.
|
|
28
|
+
|
|
29
|
+
An unresolved name reports that the skill is unknown or no longer available. Invalid names and skills whose `invocation.modelInvocable` is `false` produce distinct error results. `invocation.userInvocable` does not restrict this model-facing surface.
|
|
30
|
+
|
|
31
|
+
Tool execution does not add a synthetic context message. Its freshly loaded result is already recorded as the tool result and becomes available to the next model step without duplicating the body. Only the catalog projection adds replacement summaries.
|
|
32
|
+
|
|
33
|
+
## Model Experience
|
|
34
|
+
|
|
35
|
+
### Session catalog
|
|
36
|
+
|
|
37
|
+
#### What the model sees
|
|
38
|
+
|
|
39
|
+
If model-invocable skills exist and this exact `skill` tool is visible, the agent receives the catalog template below as a durable user-role message before the first request, with one data-dependent entry per sorted skill. Later membership, description, or visibility changes append a complete replacement using the same `<available_skills>` envelope; deleting every skill appends an empty envelope with an explicit instruction not to use older names. The template's closing sentence is the rule against double-loading: the user-explicit gesture boundary (the pre-step listener below) injects the same `renderSkillContent` output (shared from `@deepseek-ai/dsh-skill`) inline, and the catalog tells the model to follow that block instead of re-loading the skill through the tool; the replacement-catalog template carries the same sentence in both arms, including the emptied catalog.
|
|
40
|
+
|
|
41
|
+
##### Skill catalog template
|
|
42
|
+
|
|
43
|
+
```markdown
|
|
44
|
+
<system-reminder>
|
|
45
|
+
A skill is a reusable set of task-specific instructions. The following skills are available in this session:
|
|
46
|
+
|
|
47
|
+
<available_skills>
|
|
48
|
+
- `<name>`: <normalized-and-capped-description>
|
|
49
|
+
</available_skills>
|
|
50
|
+
|
|
51
|
+
If the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.
|
|
52
|
+
A user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill.
|
|
53
|
+
</system-reminder>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### Token effect
|
|
57
|
+
|
|
58
|
+
Repeated input cost scales with skill count and `catalogDescriptionMaxLength`; no initial catalog tokens are sent when the list is empty or the tool is hidden or shadowed. Each actual catalog change adds one retained complete replacement message.
|
|
59
|
+
|
|
60
|
+
#### KV Cache effect
|
|
61
|
+
|
|
62
|
+
The initial durable catalog is appended after the existing reusable prefix. Dynamic changes are append-only history after that catalog, so earlier reusable tokens stay intact while each newly appended catalog and later turns form a new suffix. A new or resumed instance with a changed digest may affect cache reuse from the newly appended catalog position.
|
|
63
|
+
|
|
64
|
+
### Tool schema
|
|
65
|
+
|
|
66
|
+
#### What the model sees
|
|
67
|
+
|
|
68
|
+
The model sees the generated [`skill` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-skill).
|
|
69
|
+
|
|
70
|
+
#### Token effect
|
|
71
|
+
|
|
72
|
+
Fixed schema cost per request where the tool is visible.
|
|
73
|
+
|
|
74
|
+
#### KV Cache effect
|
|
75
|
+
|
|
76
|
+
Prefix-stable while the tool definition and visibility are unchanged. Shadowing, restrictions, or plugin lifecycle changes may invalidate reuse from this schema.
|
|
77
|
+
|
|
78
|
+
### Tool result
|
|
79
|
+
|
|
80
|
+
#### What the model sees
|
|
81
|
+
|
|
82
|
+
A successful call uses the result template and the provider-managed, directory, URL, or opaque resource guidance below.
|
|
83
|
+
|
|
84
|
+
##### Skill result template
|
|
85
|
+
|
|
86
|
+
```markdown
|
|
87
|
+
<skill_content name="<escaped-name>">
|
|
88
|
+
<skill_resources>
|
|
89
|
+
<resource-guidance>
|
|
90
|
+
</skill_resources>
|
|
91
|
+
|
|
92
|
+
<skill_instructions>
|
|
93
|
+
<provider-owned-instruction-body>
|
|
94
|
+
</skill_instructions>
|
|
95
|
+
</skill_content>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
##### Provider-managed resource guidance
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
Resources for this skill are managed by provider "<provider>".
|
|
102
|
+
Load referenced resources only as needed.
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
##### Directory resource guidance
|
|
106
|
+
|
|
107
|
+
```markdown
|
|
108
|
+
Base directory for this skill: <path>
|
|
109
|
+
Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
##### URL resource guidance
|
|
113
|
+
|
|
114
|
+
```markdown
|
|
115
|
+
Base URL for this skill: <url>
|
|
116
|
+
Resolve relative URLs mentioned by this skill against the base URL before using them. Load referenced resources only as needed.
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
##### Opaque resource guidance
|
|
120
|
+
|
|
121
|
+
```markdown
|
|
122
|
+
Resources for this skill: <description>
|
|
123
|
+
Load referenced resources only as needed.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### Token effect
|
|
127
|
+
|
|
128
|
+
Loaded instructions are data-dependent tool-result tokens, resent on later steps until compaction; no duplicate `agent.inject()` copy is made.
|
|
129
|
+
|
|
130
|
+
#### KV Cache effect
|
|
131
|
+
|
|
132
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
133
|
+
|
|
134
|
+
### Tool errors
|
|
135
|
+
|
|
136
|
+
#### What the model sees
|
|
137
|
+
|
|
138
|
+
Invalid or stale selections return exactly `Error: invalid skill name "<name>"`, `Error: skill "<name>" is unknown or no longer available`, or `Error: skill "<name>" is not available for model invocation`. Provider-thrown lookup text is data-dependent and receives the same `Error: <message>` wrapper.
|
|
139
|
+
|
|
140
|
+
#### Token effect
|
|
141
|
+
|
|
142
|
+
Only a failing call adds these retained tokens.
|
|
143
|
+
|
|
144
|
+
#### KV Cache effect
|
|
145
|
+
|
|
146
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
147
|
+
|
|
148
|
+
### User-explicit invocation injection
|
|
149
|
+
|
|
150
|
+
#### What the model sees
|
|
151
|
+
|
|
152
|
+
A whitespace-bounded `/name` token anywhere in a claimed user message, naming a user-invocable skill in the workspace catalog, injects that skill's full `<skill_content>` rendering (the exact result-template shape above) as a `user`-role instructions context appended after every other injection of that step — background first, the material to act on last. Only direct user input is scanned, the check runs on the loaded definition, and unknown or user-disabled names stay ordinary prose. This is the sole entry point for `disable-model-invocation` skills, which the catalog and the `skill` tool never expose; the catalog's closing sentence tells the model to follow the injected block instead of re-loading it.
|
|
153
|
+
|
|
154
|
+
#### Token effect
|
|
155
|
+
|
|
156
|
+
Each gesture adds one rendered skill body to that turn as injected context — the same size as the tool result for the same skill, paid deterministically at the user's request instead of at the model's discretion. Repeated gestures for one skill within one step inject once.
|
|
157
|
+
|
|
158
|
+
#### KV Cache effect
|
|
159
|
+
|
|
160
|
+
Append-only; the injection lands after the reusable request prefix inside the step's message batch and does not invalidate existing KV-cache entries.
|
|
161
|
+
|
|
162
|
+
## Known Limitations and Deferred Work
|
|
163
|
+
|
|
164
|
+
- **The catalog omits `whenToUse`, source, and provider metadata** — routing is based only on name and a capped description; `whenToUse` remains provider metadata and is not rendered by the loaded wrapper either.
|
|
165
|
+
- **Loaded instruction bodies have no size cap** — a provider can return a skill large enough to consume substantial next-step context; only catalog descriptions are truncated.
|
|
166
|
+
- **Resources are guidance, not attachments** — the tool reports a base directory/URL/opaque hint but neither enumerates nor fetches referenced files for the model.
|
|
167
|
+
- **Loading is one-shot text** — there is no partial, streaming, or cached-content handle when a remote provider is slow or a skill body is large.
|
|
168
|
+
- **Catalog replacement is whole-list** — one changed name or description appends every currently visible summary; this keeps stale-name retirement explicit but costs tokens proportional to the catalog.
|
|
169
|
+
- **Bodies are not versioned** — body-only edits do not change the catalog digest or notify the model; a later tool call reads the current provider content while earlier tool results remain historical facts.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-tool-skill
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
面向模型的 skill(技能)目录和 `skill` 工具。
|
|
6
|
+
|
|
7
|
+
需要 `ctx.agents`、`ctx.tools` 和 `ctx.skills`(`inject: ['agents', 'tools', 'skills']`)。
|
|
8
|
+
|
|
9
|
+
## 目录生命周期
|
|
10
|
+
|
|
11
|
+
每次符合条件的 `agent/pre-step`,该插件都会使用调用会话的 cwd 调用 `ctx.skills.snapshot()`,将 pre-step 中止信号转发到发现流程,应用 `skill` 工具的精确可见性,并按顺序渲染 `name` 和 `description` 条目。如果先前不存在目录且该视图非空,插件会向下游 `enter` 决策添加初始的持久用户角色 `<system-reminder>`。目录消息只包含这些摘要;skill 正文、路径、来源、提供方和 `whenToUse` 提示仍位于目录之外。
|
|
12
|
+
|
|
13
|
+
每条目录消息都携带 `skill-catalog` 来源,也就是 `catalog` 形态的上下文。它的 `entries` 精确记录本次发布的 `name` 与 `description` 对,替换目录另带 `update`。digest 覆盖这些持久条目,而不是渲染后的正文,因此 `<system-reminder>` 包装不会影响是否需要重新发布,消费方也不需要重新解析 `<available_skills>` 块。插件从后向前扫描持久会话事件且不复制,并以最新一条仍可见且可读的 `skill-catalog` 消息作为比较基线;不可读和外来的记录都会跳过。digest 变化时,下游 `enter` 决策会收到一条包含完整替换目录的持久用户角色消息;空替换会显式停用较早的名称。如果没有目录仍然可见,但历史中存在可识别目录,则说明压缩(compaction)已将其遮蔽,下一次完整观察会重新建立当前目录。提供方快照不完整时,插件不会发送任何内容,并会保留最后一次完整的模型视图,在下一次 pre-step 重试。若不存在先前目录且当前视图为空,则不需要 tombstone。
|
|
14
|
+
|
|
15
|
+
如果最初没有模型可调用 skill,则省略目录;如果该 agent(智能体)的工具视图排除了随附的 `skill` 工具,或解析出同名的作用域内遮蔽项,也会省略目录。身份比对针对本插件所注册的那个定义,而非按自身名字回查,因此本插件既可全局挂载,也可挂在单个 agent 的组装内——在后者中 `register()` 只归档进该 agent 的分层。可见性变更参与 digest 计算,使提示词指引、模型可见 schema 和可执行分派保持对齐。
|
|
16
|
+
|
|
17
|
+
`catalogDescriptionMaxLength` 控制规范化后的目录描述,渲染时会对其执行 XML 转义。其默认值是 `500`,且必须是不小于 `3` 的整数,以便为截断省略号保留空间。[skill 目录热刷新 Agent Note](../../../.agents/notes/implemented/feature/2026-07-27-skill-catalog-hot-refresh.md) 负责定义持久初始目录和替换目录的生命周期。
|
|
18
|
+
|
|
19
|
+
## 工具:`skill`
|
|
20
|
+
|
|
21
|
+
| 参数 | 类型 | 说明 |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `name` | string(必填) | 可用 skill 列表中精确的 kebab-case skill 名称。 |
|
|
24
|
+
|
|
25
|
+
执行使用调用 agent 的 `session.header.cwd`,使结果随工作区变化的提供方能够解析出胜出的 skill。成功调用返回规范形式的 `{ name, provider, resourceBase?, content }`,其中不包含目录排名和提供方内部机制;其 Native 渲染器会生成一个文本结果,其中包含 `<skill_content name="...">`、`<skill_resources>` 和 `<skill_instructions>`。
|
|
26
|
+
|
|
27
|
+
资源指引只会根据 `resourceBase` 解析指令显式引用的路径或 URL;脚本、参考资料和资源文件按需加载,结果不会列举 skill 目录。本地提供方可以提供目录,而远程或嵌入式提供方可以提供 URL 或不透明加载指引。
|
|
28
|
+
|
|
29
|
+
无法解析的名称会报告 skill 未知或已不可用。无效名称和 `invocation.modelInvocable` 为 `false` 的 skill 会产生不同的错误结果。`invocation.userInvocable` 不限制这个面向模型的接口。
|
|
30
|
+
|
|
31
|
+
工具执行不会添加合成上下文消息。新加载的结果已作为工具结果记录,并在下一个模型步骤可用,无需重复正文。只有目录投影会添加替换摘要。
|
|
32
|
+
|
|
33
|
+
## 模型体验
|
|
34
|
+
|
|
35
|
+
### 会话目录
|
|
36
|
+
|
|
37
|
+
#### 模型看到的内容
|
|
38
|
+
|
|
39
|
+
如果存在模型可调用 skill,且可见的正是这个 `skill` 工具,agent 会在第一个请求之前收到下方目录模板,其中包含每个已排序 skill 的一条随数据而定的条目。该目录是一条持久的用户角色消息。后续成员关系、描述或可见性的变化会使用同一个 `<available_skills>` 信封追加完整替换;删除所有 skill 时,会追加一个空信封,并明确指示不得使用旧名称。模板的结尾一句是防止双重加载的规则:用户显式的手势边界(下文的 pre-step 监听器)会把同一份 `renderSkillContent` 输出(共享自 `@deepseek-ai/dsh-skill`)内联注入,目录则告诉模型遵循该块,而不是再经工具重新加载该 skill;替换目录模板的两个臂——包括清空后的目录——都携带同一句话。
|
|
40
|
+
|
|
41
|
+
##### Skill 目录模板
|
|
42
|
+
|
|
43
|
+
```markdown
|
|
44
|
+
<system-reminder>
|
|
45
|
+
A skill is a reusable set of task-specific instructions. The following skills are available in this session:
|
|
46
|
+
|
|
47
|
+
<available_skills>
|
|
48
|
+
- `<name>`: <normalized-and-capped-description>
|
|
49
|
+
</available_skills>
|
|
50
|
+
|
|
51
|
+
If the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.
|
|
52
|
+
A user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill.
|
|
53
|
+
</system-reminder>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### Token 影响
|
|
57
|
+
|
|
58
|
+
重复输入成本随 skill 数量和 `catalogDescriptionMaxLength` 增长;当列表为空或工具被隐藏或遮蔽时,不会发送初始目录 token。每次实际目录变更都会添加一条保留的完整替换消息。
|
|
59
|
+
|
|
60
|
+
#### KV Cache 影响
|
|
61
|
+
|
|
62
|
+
初始持久目录追加在现有可重用前缀之后。动态变更作为该目录之后的仅追加历史,因此较早的可重用 token 保持不变,每条新追加的目录和后续轮次都会形成新的后缀。新建或恢复的实例如果 digest 发生变化,可能会从新追加的目录位置起影响缓存重用。
|
|
63
|
+
|
|
64
|
+
### 工具 schema
|
|
65
|
+
|
|
66
|
+
#### 模型看到的内容
|
|
67
|
+
|
|
68
|
+
模型会看到生成的 [`skill` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-skill)。
|
|
69
|
+
|
|
70
|
+
#### Token 影响
|
|
71
|
+
|
|
72
|
+
工具可见时,每次请求都有固定的 schema token 开销。
|
|
73
|
+
|
|
74
|
+
#### KV Cache 影响
|
|
75
|
+
|
|
76
|
+
工具定义和可见性不变时,前缀稳定。遮蔽、限制或插件生命周期变更可能从该 schema 起使重用失效。
|
|
77
|
+
|
|
78
|
+
### 工具结果
|
|
79
|
+
|
|
80
|
+
#### 模型看到的内容
|
|
81
|
+
|
|
82
|
+
成功调用使用下方结果模板,以及提供方管理的资源指引、目录资源指引、URL 资源指引或不透明资源指引。
|
|
83
|
+
|
|
84
|
+
##### Skill 结果模板
|
|
85
|
+
|
|
86
|
+
```markdown
|
|
87
|
+
<skill_content name="<escaped-name>">
|
|
88
|
+
<skill_resources>
|
|
89
|
+
<resource-guidance>
|
|
90
|
+
</skill_resources>
|
|
91
|
+
|
|
92
|
+
<skill_instructions>
|
|
93
|
+
<provider-owned-instruction-body>
|
|
94
|
+
</skill_instructions>
|
|
95
|
+
</skill_content>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
##### 提供方管理的资源指引
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
Resources for this skill are managed by provider "<provider>".
|
|
102
|
+
Load referenced resources only as needed.
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
##### 目录资源指引
|
|
106
|
+
|
|
107
|
+
```markdown
|
|
108
|
+
Base directory for this skill: <path>
|
|
109
|
+
Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
##### URL 资源指引
|
|
113
|
+
|
|
114
|
+
```markdown
|
|
115
|
+
Base URL for this skill: <url>
|
|
116
|
+
Resolve relative URLs mentioned by this skill against the base URL before using them. Load referenced resources only as needed.
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
##### 不透明资源指引
|
|
120
|
+
|
|
121
|
+
```markdown
|
|
122
|
+
Resources for this skill: <description>
|
|
123
|
+
Load referenced resources only as needed.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### Token 影响
|
|
127
|
+
|
|
128
|
+
已加载指令是取决于数据的工具结果 token,并在后续步骤中重新发送,直到压缩(compaction);不会制作重复的 `agent.inject()` 副本。
|
|
129
|
+
|
|
130
|
+
#### KV Cache 影响
|
|
131
|
+
|
|
132
|
+
仅追加;新可见内容位于可重用请求前缀之后,不会使现有 KV Cache 条目失效。
|
|
133
|
+
|
|
134
|
+
### 工具错误
|
|
135
|
+
|
|
136
|
+
#### 模型看到的内容
|
|
137
|
+
|
|
138
|
+
无效或陈旧选择会精确返回 `Error: invalid skill name "<name>"`、`Error: skill "<name>" is unknown or no longer available` 或 `Error: skill "<name>" is not available for model invocation`。提供方抛出的查找文本取决于数据,并接收同一个 `Error: <message>` 包装层。
|
|
139
|
+
|
|
140
|
+
#### Token 影响
|
|
141
|
+
|
|
142
|
+
只有失败调用会添加这些已保留 token。
|
|
143
|
+
|
|
144
|
+
#### KV Cache 影响
|
|
145
|
+
|
|
146
|
+
仅追加;新可见内容位于可重用请求前缀之后,不会使现有 KV Cache 条目失效。
|
|
147
|
+
|
|
148
|
+
### 用户显式调用注入
|
|
149
|
+
|
|
150
|
+
#### 模型看到的内容
|
|
151
|
+
|
|
152
|
+
已认领用户消息中任意位置、以空白为界、指名工作区目录中某个用户可调用 skill 的 `/name` token,会把该 skill 的完整 `<skill_content>` 渲染(与上文结果模板完全相同的形态)作为 `user` 角色的指令上下文注入,追加在该步骤所有其他注入之后——背景在前,模型要着手处理的材料在最后。只扫描直接的用户输入,检查在已加载定义上进行,未知名称和用户不可调用的名称保持为普通行文。这是 `disable-model-invocation` skill 唯一的入口,目录和 `skill` 工具永不暴露这类 skill;目录的结尾一句会告诉模型遵循注入块,而不是重新加载它。
|
|
153
|
+
|
|
154
|
+
#### Token 影响
|
|
155
|
+
|
|
156
|
+
每次手势会把一份渲染后的 skill 正文作为注入上下文加进该轮次——尺寸与同一 skill 的工具结果相同,按用户的请求确定性地支付,而非由模型自行裁量。同一步骤内对同一 skill 的重复手势只注入一次。
|
|
157
|
+
|
|
158
|
+
#### KV Cache 影响
|
|
159
|
+
|
|
160
|
+
仅追加;注入落在该步骤的消息批次中、可重用请求前缀之后,不会使现有 KV Cache 条目失效。
|
|
161
|
+
|
|
162
|
+
## 已知限制与暂缓事项
|
|
163
|
+
|
|
164
|
+
- **目录省略 `whenToUse`、来源和提供方元数据**:路由只基于名称和有长度上限的描述;`whenToUse` 仍是提供方元数据,加载后的包装层也不渲染它。
|
|
165
|
+
- **已加载指令正文没有大小上限**:提供方可返回足以占用大量下一步上下文的 skill;只有目录描述会被截断。
|
|
166
|
+
- **资源是指引,而非附件**:工具报告基础目录/URL/不透明提示,但既不列举也不为模型获取引用文件。
|
|
167
|
+
- **加载是一次性文本**:远程提供方缓慢或 skill 正文很大时,不提供部分内容、流式输出或缓存内容句柄。
|
|
168
|
+
- **目录替换采用全量列表**:一个名称或描述发生变化,就会追加当前所有可见摘要;这样能显式停用陈旧名称,但 token 成本与目录大小成正比。
|
|
169
|
+
- **正文不做版本化**:仅修改正文不会改变目录 digest,也不会通知模型;后续工具调用会读取提供方的当前内容,而先前工具结果仍是历史事实。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { escapeText, isModelInvocable, isSkillName, isUserInvocable, renderSkillContent } from "@deepseek-ai/dsh-skill";
|
|
6
|
+
//#region lib/types/index.js
|
|
7
|
+
/**
|
|
8
|
+
* Durable session skill catalog and model-facing `skill` loader tool.
|
|
9
|
+
*
|
|
10
|
+
* @module @deepseek-ai/dsh-tool-skill
|
|
11
|
+
*/
|
|
12
|
+
const name = "tool-skill";
|
|
13
|
+
const inject = [
|
|
14
|
+
"agents",
|
|
15
|
+
"tools",
|
|
16
|
+
"skills"
|
|
17
|
+
];
|
|
18
|
+
const DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH = 500;
|
|
19
|
+
/** Durable entry list mirroring the rendered catalog lines, for non-model consumers. */
|
|
20
|
+
function catalogSourceEntries(skills, descriptionMaxLength) {
|
|
21
|
+
return skills.map((skill) => ({
|
|
22
|
+
name: skill.name,
|
|
23
|
+
description: catalogDescription(skill.description, descriptionMaxLength)
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
/** Validate and default the model-facing skill catalog configuration. */
|
|
27
|
+
const Config = z.object({ catalogDescriptionMaxLength: z.number().default(DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH) });
|
|
28
|
+
/**
|
|
29
|
+
* Register the model-facing skill loader and its visibility-matched
|
|
30
|
+
* durable session catalog. The catalog is emitted only when the calling agent
|
|
31
|
+
* resolves this plugin's exact tool registration; a restriction or scoped
|
|
32
|
+
* same-name shadow therefore removes both the schema and its call guidance.
|
|
33
|
+
*/
|
|
34
|
+
function apply(ctx, config = {}) {
|
|
35
|
+
const catalogDescriptionMaxLength = config.catalogDescriptionMaxLength ?? DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH;
|
|
36
|
+
assertPositiveInteger("catalogDescriptionMaxLength", catalogDescriptionMaxLength, 3);
|
|
37
|
+
const skillTool = defineTool({
|
|
38
|
+
name: "skill",
|
|
39
|
+
description: "Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.",
|
|
40
|
+
parameters: { name: {
|
|
41
|
+
type: "string",
|
|
42
|
+
required: true,
|
|
43
|
+
description: "The exact skill name from the available skills list."
|
|
44
|
+
} },
|
|
45
|
+
output: {
|
|
46
|
+
schema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
properties: {
|
|
50
|
+
name: {
|
|
51
|
+
type: "string",
|
|
52
|
+
required: true
|
|
53
|
+
},
|
|
54
|
+
provider: {
|
|
55
|
+
type: "string",
|
|
56
|
+
required: true
|
|
57
|
+
},
|
|
58
|
+
resourceBase: { oneOf: [
|
|
59
|
+
{
|
|
60
|
+
type: "object",
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
properties: {
|
|
63
|
+
kind: {
|
|
64
|
+
type: "string",
|
|
65
|
+
required: true,
|
|
66
|
+
const: "directory"
|
|
67
|
+
},
|
|
68
|
+
path: {
|
|
69
|
+
type: "string",
|
|
70
|
+
required: true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: "object",
|
|
76
|
+
additionalProperties: false,
|
|
77
|
+
properties: {
|
|
78
|
+
kind: {
|
|
79
|
+
type: "string",
|
|
80
|
+
required: true,
|
|
81
|
+
const: "url"
|
|
82
|
+
},
|
|
83
|
+
url: {
|
|
84
|
+
type: "string",
|
|
85
|
+
required: true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: "object",
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
properties: {
|
|
93
|
+
kind: {
|
|
94
|
+
type: "string",
|
|
95
|
+
required: true,
|
|
96
|
+
const: "opaque"
|
|
97
|
+
},
|
|
98
|
+
description: {
|
|
99
|
+
type: "string",
|
|
100
|
+
required: true
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
] },
|
|
105
|
+
content: {
|
|
106
|
+
type: "string",
|
|
107
|
+
required: true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
render: (_args, value) => [{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: renderSkillContent(value)
|
|
114
|
+
}]
|
|
115
|
+
},
|
|
116
|
+
async execute(args, exec) {
|
|
117
|
+
if (!isSkillName(args.name)) throw new Error(`invalid skill name "${args.name}"`);
|
|
118
|
+
const lookup = {
|
|
119
|
+
cwd: exec.agent?.session.header.cwd,
|
|
120
|
+
signal: exec.signal,
|
|
121
|
+
scope: exec.agent
|
|
122
|
+
};
|
|
123
|
+
const summary = (await ctx.skills.list(lookup)).find((skill) => skill.name === args.name);
|
|
124
|
+
if (!summary) throw new Error(`skill "${args.name}" is unknown or no longer available`);
|
|
125
|
+
if (!isModelInvocable(summary)) throw new Error(`skill "${args.name}" is not available for model invocation`);
|
|
126
|
+
const skill = await ctx.skills.get(args.name, lookup);
|
|
127
|
+
if (!skill) throw new Error(`skill "${args.name}" is unknown or no longer available`);
|
|
128
|
+
if (!isModelInvocable(skill)) throw new Error(`skill "${args.name}" is not available for model invocation`);
|
|
129
|
+
return {
|
|
130
|
+
name: skill.name,
|
|
131
|
+
provider: skill.provider,
|
|
132
|
+
...skill.resourceBase !== void 0 ? { resourceBase: { ...skill.resourceBase } } : {},
|
|
133
|
+
content: skill.content
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
presentCall(args) {
|
|
137
|
+
return {
|
|
138
|
+
card: "generic",
|
|
139
|
+
title: `Load skill ${args.name}`,
|
|
140
|
+
kind: "read",
|
|
141
|
+
rawInput: args.name
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
ctx.tools.register(skillTool);
|
|
146
|
+
ctx.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
|
|
147
|
+
const decision = await next();
|
|
148
|
+
if (decision.kind === "reject") return decision;
|
|
149
|
+
const names = invokedSkillNames(messages);
|
|
150
|
+
if (names.length === 0) return decision;
|
|
151
|
+
signal.throwIfAborted();
|
|
152
|
+
const lookup = {
|
|
153
|
+
cwd: agent.session.header.cwd,
|
|
154
|
+
signal,
|
|
155
|
+
scope: agent
|
|
156
|
+
};
|
|
157
|
+
const injections = [];
|
|
158
|
+
for (const name of names) {
|
|
159
|
+
const skill = await ctx.skills.get(name, lookup);
|
|
160
|
+
signal.throwIfAborted();
|
|
161
|
+
if (skill === void 0 || !isUserInvocable(skill)) continue;
|
|
162
|
+
const source = {
|
|
163
|
+
kind: "skill-invocation",
|
|
164
|
+
name,
|
|
165
|
+
form: "instructions"
|
|
166
|
+
};
|
|
167
|
+
injections.push(createUserMessage({
|
|
168
|
+
content: [{
|
|
169
|
+
type: "text",
|
|
170
|
+
text: renderSkillContent(skill)
|
|
171
|
+
}],
|
|
172
|
+
source
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
if (injections.length === 0) return decision;
|
|
176
|
+
return {
|
|
177
|
+
kind: "enter",
|
|
178
|
+
messages: [...decision.messages, ...injections]
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
ctx.on("agent/pre-step", async ({ agent, signal }, next) => {
|
|
182
|
+
const decision = await next();
|
|
183
|
+
if (decision.kind === "reject") return decision;
|
|
184
|
+
signal.throwIfAborted();
|
|
185
|
+
const snapshot = ctx.tools.get(skillTool.name, agent) === skillTool ? await ctx.skills.snapshot({
|
|
186
|
+
cwd: agent.session.header.cwd,
|
|
187
|
+
signal,
|
|
188
|
+
scope: agent
|
|
189
|
+
}) : {
|
|
190
|
+
skills: [],
|
|
191
|
+
complete: true
|
|
192
|
+
};
|
|
193
|
+
signal.throwIfAborted();
|
|
194
|
+
if (!snapshot.complete) return decision;
|
|
195
|
+
const skills = snapshot.skills.filter(isModelInvocable);
|
|
196
|
+
const entries = catalogSourceEntries(skills, catalogDescriptionMaxLength);
|
|
197
|
+
const digest = digestCatalogEntries(entries);
|
|
198
|
+
const history = catalogHistory(agent);
|
|
199
|
+
const existing = catalogMessage(decision.messages);
|
|
200
|
+
if (history.visibleDigest === digest) return existing === void 0 ? decision : {
|
|
201
|
+
kind: "enter",
|
|
202
|
+
messages: decision.messages.filter((message) => message.id !== existing.message.id)
|
|
203
|
+
};
|
|
204
|
+
if (existing !== void 0 && digestCatalogEntries(existing.entries) === digest) return decision;
|
|
205
|
+
if (!history.published && skills.length === 0) return existing === void 0 ? decision : {
|
|
206
|
+
kind: "enter",
|
|
207
|
+
messages: decision.messages.filter((message) => message.id !== existing.message.id)
|
|
208
|
+
};
|
|
209
|
+
const catalog = history.published ? renderCatalogUpdate(entries) : renderCatalogMessage(entries);
|
|
210
|
+
return {
|
|
211
|
+
kind: "enter",
|
|
212
|
+
messages: existing === void 0 ? [...decision.messages, catalog] : decision.messages.map((message) => message.id === existing.message.id ? catalog : message)
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function renderCatalogMessage(entries) {
|
|
217
|
+
return createUserMessage({
|
|
218
|
+
content: [{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: [
|
|
221
|
+
"<system-reminder>",
|
|
222
|
+
"A skill is a reusable set of task-specific instructions. The following skills are available in this session:",
|
|
223
|
+
"",
|
|
224
|
+
"<available_skills>",
|
|
225
|
+
...renderCatalogEntries(entries),
|
|
226
|
+
"</available_skills>",
|
|
227
|
+
"",
|
|
228
|
+
"If the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.",
|
|
229
|
+
"A user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill.",
|
|
230
|
+
"</system-reminder>"
|
|
231
|
+
].join("\n")
|
|
232
|
+
}],
|
|
233
|
+
source: {
|
|
234
|
+
kind: "skill-catalog",
|
|
235
|
+
form: "catalog",
|
|
236
|
+
entries
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function renderCatalogUpdate(entries) {
|
|
241
|
+
const availability = entries.length === 0 ? ["No skills are currently available through the `skill` tool. Do not use names from earlier skill catalogs.", "A user may still invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the `skill` tool for it."] : ["Use only names in this replacement catalog. If the user names a listed skill, or the task clearly matches its description, call the `skill` tool with the exact name before acting.", "A user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill."];
|
|
242
|
+
return createUserMessage({
|
|
243
|
+
content: [{
|
|
244
|
+
type: "text",
|
|
245
|
+
text: [
|
|
246
|
+
"<system-reminder>",
|
|
247
|
+
"The available skill catalog changed. This complete catalog replaces every earlier available-skills list in this session:",
|
|
248
|
+
"",
|
|
249
|
+
"<available_skills>",
|
|
250
|
+
...renderCatalogEntries(entries),
|
|
251
|
+
"</available_skills>",
|
|
252
|
+
"",
|
|
253
|
+
...availability,
|
|
254
|
+
"</system-reminder>"
|
|
255
|
+
].join("\n")
|
|
256
|
+
}],
|
|
257
|
+
source: {
|
|
258
|
+
kind: "skill-catalog",
|
|
259
|
+
form: "catalog",
|
|
260
|
+
update: true,
|
|
261
|
+
entries
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Model-facing catalog lines, projected from the same entries the source records.
|
|
267
|
+
* The pseudo-XML escaping belongs to this frame, not to the published fact, so it
|
|
268
|
+
* is applied here and never stored. Names are `isSkillName`-validated and carry
|
|
269
|
+
* no escapable character.
|
|
270
|
+
*/
|
|
271
|
+
function renderCatalogEntries(entries) {
|
|
272
|
+
return entries.map((entry) => `- \`${entry.name}\`: ${escapeText(entry.description)}`);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Catalog identity over the durable entry list rather than the rendered prose.
|
|
276
|
+
* The entries are what changes; the surrounding `<system-reminder>` framing is
|
|
277
|
+
* written for the model and must not decide whether a republish is needed.
|
|
278
|
+
*/
|
|
279
|
+
function digestCatalogEntries(entries) {
|
|
280
|
+
const canonical = entries.map((entry) => JSON.stringify([entry.name, entry.description])).join("\n");
|
|
281
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Entries of one durable catalog message, or undefined when the record is not a
|
|
285
|
+
* usable catalog.
|
|
286
|
+
*
|
|
287
|
+
* `agent.session.events` may be a resumed, forked, or externally written seed,
|
|
288
|
+
* and seed validation only guarantees a source object with a non-empty `kind`;
|
|
289
|
+
* no per-kind field is checked there. An unreadable record is therefore treated
|
|
290
|
+
* as "not this plugin's catalog" — the posture the replaced content digest had —
|
|
291
|
+
* rather than throwing inside the step listener, which would fail every
|
|
292
|
+
* subsequent turn of that session.
|
|
293
|
+
*/
|
|
294
|
+
function readCatalogEntries(source) {
|
|
295
|
+
const entries = source.entries;
|
|
296
|
+
if (!Array.isArray(entries)) return void 0;
|
|
297
|
+
const readable = [];
|
|
298
|
+
for (const entry of entries) {
|
|
299
|
+
if (typeof entry !== "object" || entry === null) return void 0;
|
|
300
|
+
const { name, description } = entry;
|
|
301
|
+
if (typeof name !== "string" || name === "" || typeof description !== "string") return void 0;
|
|
302
|
+
readable.push({
|
|
303
|
+
name,
|
|
304
|
+
description
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return readable;
|
|
308
|
+
}
|
|
309
|
+
function catalogHistory(agent) {
|
|
310
|
+
const visible = new Set(agent.session.surface.nodes);
|
|
311
|
+
const events = agent.session.events;
|
|
312
|
+
let published = false;
|
|
313
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
314
|
+
const event = events[index];
|
|
315
|
+
if (event.type !== "user/message" || event.data.source.kind !== "skill-catalog") continue;
|
|
316
|
+
const entries = readCatalogEntries(event.data.source);
|
|
317
|
+
if (entries === void 0) continue;
|
|
318
|
+
const digest = digestCatalogEntries(entries);
|
|
319
|
+
published = true;
|
|
320
|
+
if (visible.has(event.seq)) return {
|
|
321
|
+
visibleDigest: digest,
|
|
322
|
+
published
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return { published };
|
|
326
|
+
}
|
|
327
|
+
function catalogMessage(messages) {
|
|
328
|
+
for (const message of messages) {
|
|
329
|
+
if (message.source.kind !== "skill-catalog") continue;
|
|
330
|
+
const entries = readCatalogEntries(message.source);
|
|
331
|
+
if (entries !== void 0) return {
|
|
332
|
+
message,
|
|
333
|
+
entries
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/** Normalized, length-bounded description exactly as the catalog publishes it (unescaped). */
|
|
338
|
+
function catalogDescription(value, maxLength) {
|
|
339
|
+
const normalized = value.replaceAll(/\s+/g, " ").trim();
|
|
340
|
+
return normalized.length <= maxLength ? normalized : `${normalized.slice(0, maxLength - 3)}...`;
|
|
341
|
+
}
|
|
342
|
+
function assertPositiveInteger(name, value, minimum = 1) {
|
|
343
|
+
if (!Number.isInteger(value) || value < minimum) throw new Error(`tool-skill: ${name} must be an integer greater than or equal to ${minimum}`);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* A whitespace-bounded `/name` token (the public skill-name grammar) anywhere
|
|
347
|
+
* in the text — the same word-boundary shape the transcript chip decoration
|
|
348
|
+
* uses, so a gesture reads as one wherever it sits in the sentence. A second
|
|
349
|
+
* `/` or any non-boundary character breaks the match, which keeps file paths
|
|
350
|
+
* (`/usr/bin`) and fractions (`5/8`) out.
|
|
351
|
+
*/
|
|
352
|
+
const SKILL_GESTURE = /(^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?=\s|$)/g;
|
|
353
|
+
/**
|
|
354
|
+
* `/name` gesture tokens from the claimed user messages, deduplicated in
|
|
355
|
+
* first-seen order. Every text block of direct user input is scanned; no
|
|
356
|
+
* other source can forge a gesture.
|
|
357
|
+
* @param messages - the step's claimed batch.
|
|
358
|
+
* @returns candidate skill names, unvalidated against the registry.
|
|
359
|
+
*/
|
|
360
|
+
function invokedSkillNames(messages) {
|
|
361
|
+
const names = [];
|
|
362
|
+
for (const message of messages) {
|
|
363
|
+
if (message.source.kind !== "user") continue;
|
|
364
|
+
for (const block of message.content) {
|
|
365
|
+
if (block.type !== "text") continue;
|
|
366
|
+
for (const match of block.text.matchAll(SKILL_GESTURE)) {
|
|
367
|
+
const name = match[2];
|
|
368
|
+
if (name !== void 0 && !names.includes(name)) names.push(name);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return names;
|
|
373
|
+
}
|
|
374
|
+
//#endregion
|
|
375
|
+
export { Config, apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-tool-skill`.
|
|
4
|
+
* @module @deepseek-ai/dsh-tool-skill/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-tool-skill";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "tool-skill-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution
|
|
13
|
+
* relations are owned by the capability seam it calls.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable session skill catalog and model-facing `skill` loader tool.
|
|
3
|
+
*
|
|
4
|
+
* @module @deepseek-ai/dsh-tool-skill
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
7
|
+
import z from '@deepseek-ai/schemastery';
|
|
8
|
+
export declare const name = "tool-skill";
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Durable provider and item records for one published session skill catalog. The catalog is a
|
|
12
|
+
* `catalog`-form context, so it records the entries it published beside the
|
|
13
|
+
* model-facing prose: a consumer presenting the list must not re-parse the
|
|
14
|
+
* `<available_skills>` block, whose framing exists for the model.
|
|
15
|
+
*/
|
|
16
|
+
export interface SkillCatalogSource {
|
|
17
|
+
readonly kind: 'skill-catalog';
|
|
18
|
+
readonly form: 'catalog';
|
|
19
|
+
/** Marks a replacement catalog rather than this session's first publication. */
|
|
20
|
+
readonly update?: true;
|
|
21
|
+
/** Exactly the entries this message published, in catalog order. */
|
|
22
|
+
readonly entries: readonly {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly description: string;
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
declare module '@deepseek-ai/dsh-llm' {
|
|
28
|
+
interface MessageSourceMap {
|
|
29
|
+
'skill-catalog': SkillCatalogSource;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Model-facing skill catalog configuration. */
|
|
33
|
+
export interface Config {
|
|
34
|
+
/** Maximum normalized description length rendered in the session catalog; minimum 3. */
|
|
35
|
+
catalogDescriptionMaxLength?: number;
|
|
36
|
+
}
|
|
37
|
+
/** Validate and default the model-facing skill catalog configuration. */
|
|
38
|
+
export declare const Config: z<Config>;
|
|
39
|
+
/**
|
|
40
|
+
* Register the model-facing skill loader and its visibility-matched
|
|
41
|
+
* durable session catalog. The catalog is emitted only when the calling agent
|
|
42
|
+
* resolves this plugin's exact tool registration; a restriction or scoped
|
|
43
|
+
* same-name shadow therefore removes both the schema and its call guidance.
|
|
44
|
+
*/
|
|
45
|
+
export declare function apply(ctx: Context, config?: Config): void;
|
|
46
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-tool-skill`.
|
|
3
|
+
* @module @deepseek-ai/dsh-tool-skill/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "tool-skill-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-tool-skill",
|
|
3
|
+
"description": "Model-facing skill loading tool for the DeepSeek Harness",
|
|
4
|
+
"version": "0.0.1-rc.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "restricted"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/skill/tool-skill"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "BSD-3-Clause",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
|
|
36
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
37
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
|
|
38
|
+
"@deepseek-ai/dsh-skill": "^0.0.1-rc.1",
|
|
39
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1",
|
|
40
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.1"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@deepseek-ai/schemastery": "^3.18.1-rc.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
47
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-scope": "^0.0.1-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1",
|
|
50
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
|
|
51
|
+
"@deepseek-ai/dsh-skill": "^0.0.1-rc.1",
|
|
52
|
+
"@deepseek-ai/dsh-skill-local": "^0.0.1-rc.1",
|
|
53
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
|
|
54
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
55
|
+
}
|
|
56
|
+
}
|