fix(skill): normalize invocation policy
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/skill/skill-local/README.md
|
||||
README.md: 56e19088e075f0ce9ad76eebdd23054e538174e4
|
||||
README.zh.md: 38caf2716240700aab4220783068651c9dd851f7
|
||||
README.md: cbef5e489e4340e10f3337ffd1bdb64490af4bc1
|
||||
README.zh.md: 492aad6ef6d16149b9256de3f6830e451777f7d1
|
||||
|
||||
@@ -38,7 +38,7 @@ When `ctx.fs` is available, discovery lists roots through `ctx.fs.listDir`, read
|
||||
|
||||
Skills can be single-level directory bundles (`<name>/SKILL.md`) or flat Markdown files (`<name>.md`). Nested `**/SKILL.md` discovery is intentionally not part of v1. Frontmatter is parsed as an open YAML object with the `yaml` package; this provider currently interprets required `name` and `description`, plus optional `whenToUse`, `metadata`, `disable-model-invocation`, and `user-invocable`. Names must be kebab-case.
|
||||
|
||||
The two invocation fields accept YAML booleans and the case-insensitive forms `true`/`false`, `yes`/`no`, `on`/`off`, and `1`/`0`. `disable-model-invocation: true` excludes the skill from model-facing catalogs and loaders; `user-invocable: false` excludes it from human-facing commands. Omitted fields preserve both forms of invocation. The camel-case spellings `disableModelInvocation` and `userInvocable` are rejected with a warning instead of acting as compatibility aliases.
|
||||
The two invocation fields accept YAML booleans and the case-insensitive forms `true`/`false`, `yes`/`no`, `on`/`off`, and `1`/`0`. `disable-model-invocation: true` excludes the skill from model-facing catalogs and loaders; `user-invocable: false` excludes it from human-facing commands. If either field is present, the provider fills both positive internal policy values from these external defaults. The camel-case spellings `disableModelInvocation`, `modelInvocable`, and `userInvocable` are rejected with a warning instead of acting as compatibility aliases.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
Skill 可以是单层目录 bundle(`<name>/SKILL.md`),也可以是平铺 Markdown 文件(`<name>.md`)。v1 刻意不包含嵌套 `**/SKILL.md` 发现。Frontmatter 使用 `yaml` 包解析为开放的 YAML 对象;该提供方目前解析必填的 `name` 和 `description`,以及可选的 `whenToUse`、`metadata`、`disable-model-invocation` 和 `user-invocable`。名称必须使用 kebab-case。
|
||||
|
||||
这两个调用字段接受 YAML 布尔值,以及不区分大小写的 `true`/`false`、`yes`/`no`、`on`/`off` 和 `1`/`0`。`disable-model-invocation: true` 会从面向模型的目录和加载器中排除该 skill;`user-invocable: false` 会从面向用户的命令中排除该 skill。省略字段时保留两种调用方式。系统会拒绝驼峰形式的 `disableModelInvocation` 和 `userInvocable` 并记录警告,而不会将其作为兼容别名。
|
||||
这两个调用字段接受 YAML 布尔值,以及不区分大小写的 `true`/`false`、`yes`/`no`、`on`/`off` 和 `1`/`0`。`disable-model-invocation: true` 会从面向模型的目录和加载器中排除该 skill;`user-invocable: false` 会从面向用户的命令中排除该 skill。任一字段存在时,提供方都会按照这些外部默认值填充两个正向内部策略值。系统会拒绝驼峰形式的 `disableModelInvocation`、`modelInvocable` 和 `userInvocable` 并记录警告,而不会将其作为兼容别名。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -430,13 +430,14 @@ function optionalString(data: Record<string, unknown>, key: string): { [K in typ
|
||||
|
||||
function parseInvocationPolicy(data: Record<string, unknown>): SkillInvocationPolicy | undefined {
|
||||
rejectLegacyInvocationKey(data, 'disableModelInvocation', 'disable-model-invocation')
|
||||
rejectLegacyInvocationKey(data, 'modelInvocable', 'disable-model-invocation')
|
||||
rejectLegacyInvocationKey(data, 'userInvocable', 'user-invocable')
|
||||
const disableModelInvocation = frontmatterBoolean(data, 'disable-model-invocation')
|
||||
const userInvocable = frontmatterBoolean(data, 'user-invocable')
|
||||
if (disableModelInvocation === undefined && userInvocable === undefined) return undefined
|
||||
return {
|
||||
...disableModelInvocation === undefined ? {} : { disableModelInvocation },
|
||||
...userInvocable === undefined ? {} : { userInvocable },
|
||||
modelInvocable: disableModelInvocation !== true,
|
||||
userInvocable: userInvocable !== false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -246,16 +246,16 @@ describe('LocalSkillProvider', () => {
|
||||
])
|
||||
expect(await ctx.skills.get('flat-skill')).toBeUndefined()
|
||||
expect(await ctx.skills.get('user-only-skill')).toMatchObject({
|
||||
invocation: { disableModelInvocation: true },
|
||||
invocation: { modelInvocable: false, userInvocable: true },
|
||||
content: 'User-only.',
|
||||
})
|
||||
expect(await ctx.skills.get('model-only-skill')).toMatchObject({
|
||||
invocation: { userInvocable: false },
|
||||
invocation: { modelInvocable: true, userInvocable: false },
|
||||
content: 'Model-only.',
|
||||
})
|
||||
expect(await ctx.skills.get('rich-skill')).toMatchObject({
|
||||
whenToUse: 'For richer local parsing',
|
||||
invocation: { disableModelInvocation: false, userInvocable: true },
|
||||
invocation: { modelInvocable: true, userInvocable: true },
|
||||
metadata: { owner: 'tests' },
|
||||
})
|
||||
expect(await ctx.skills.get('Bad_Name')).toBeUndefined()
|
||||
@@ -293,10 +293,16 @@ describe('LocalSkillProvider', () => {
|
||||
const ctx = await setupLocal(home)
|
||||
|
||||
for (const [index] of truthy.entries()) {
|
||||
expect((await ctx.skills.get(`truthy-${index}`))?.invocation).toEqual({ disableModelInvocation: true })
|
||||
expect((await ctx.skills.get(`truthy-${index}`))?.invocation).toEqual({
|
||||
modelInvocable: false,
|
||||
userInvocable: true,
|
||||
})
|
||||
}
|
||||
for (const [index] of falsy.entries()) {
|
||||
expect((await ctx.skills.get(`falsy-${index}`))?.invocation).toEqual({ userInvocable: false })
|
||||
expect((await ctx.skills.get(`falsy-${index}`))?.invocation).toEqual({
|
||||
modelInvocable: true,
|
||||
userInvocable: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -306,6 +312,7 @@ describe('LocalSkillProvider', () => {
|
||||
await writeSkill(root, 'good-skill', 'Good skill')
|
||||
const invalid = [
|
||||
['legacy-model', 'disableModelInvocation: true'],
|
||||
['legacy-positive-model', 'modelInvocable: false'],
|
||||
['legacy-user', 'userInvocable: false'],
|
||||
['bad-string', 'disable-model-invocation: maybe'],
|
||||
['bad-value', 'user-invocable: null'],
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/skill/skill/README.md
|
||||
README.md: 30e48b57cd33c50013f857c61e63cba74fd2cd62
|
||||
README.zh.md: 08b4a97a921565b5d69960e6f69d6ee6dd194986
|
||||
README.md: fd994fb2d20d0d8027b33d1092b9a94de0375e15
|
||||
README.zh.md: 13cf124ac55604cb3a9c31e0e79ce574ff466444
|
||||
|
||||
@@ -23,16 +23,16 @@ This package owns the `ctx.skills` interface. It does not know whether skills co
|
||||
|
||||
### Invocation policy
|
||||
|
||||
`SkillSummary.invocation` is a typed policy object with optional `disableModelInvocation` and `userInvocable` booleans. Missing values preserve the default model-and-user behavior. The registry keeps all four combinations so one discovery result can serve model-facing tools, human-facing commands, and trusted internal callers without conflating their catalogs.
|
||||
`SkillSummary.invocation` is an optional typed policy object. When present, its required positive booleans `modelInvocable` and `userInvocable` describe the two surfaces independently; omitting the object preserves the default model-and-user behavior. The registry keeps all four combinations so one discovery result can serve model-facing tools, human-facing commands, and trusted internal callers without conflating their catalogs.
|
||||
|
||||
| Policy | Model | User |
|
||||
|---|---|---|
|
||||
| neither field, or `false` / `true` | included | included |
|
||||
| `userInvocable: false` | included | excluded |
|
||||
| `disableModelInvocation: true` | excluded | included |
|
||||
| both restrictive values | excluded | excluded |
|
||||
| no `invocation`, or `{ modelInvocable: true, userInvocable: true }` | included | included |
|
||||
| `{ modelInvocable: true, userInvocable: false }` | included | excluded |
|
||||
| `{ modelInvocable: false, userInvocable: true }` | excluded | included |
|
||||
| `{ modelInvocable: false, userInvocable: false }` | excluded | excluded |
|
||||
|
||||
`isModelInvocable(skill)` returns false only for `disableModelInvocation: true`; `isUserInvocable(skill)` returns false only for `userInvocable: false`. `ctx.skills.get()` remains the trusted, policy-neutral loading primitive, so every user- or model-facing consumer must enforce the predicate that matches its surface before exposing or loading a skill.
|
||||
`isModelInvocable(skill)` and `isUserInvocable(skill)` read the matching positive field, with an absent policy permitting both surfaces. `ctx.skills.get()` remains the trusted, policy-neutral loading primitive, so every user- or model-facing consumer must enforce the predicate that matches its surface before exposing or loading a skill.
|
||||
|
||||
## Provider Contract
|
||||
|
||||
|
||||
@@ -23,16 +23,16 @@
|
||||
|
||||
### 调用策略
|
||||
|
||||
`SkillSummary.invocation` 是类型化策略对象,其中包含可选的布尔字段 `disableModelInvocation` 和 `userInvocable`。字段缺失时保留模型和用户均可调用的默认行为。注册表保留全部四种组合,使一次发现结果可以同时服务面向模型的工具、面向用户的命令和受信内部调用方,而不会混淆各自的目录。
|
||||
`SkillSummary.invocation` 是一个可选的类型化策略对象。该对象存在时,其必填的正向布尔字段 `modelInvocable` 和 `userInvocable` 分别描述两个接口;省略该对象时保留模型和用户均可调用的默认行为。注册表保留全部四种组合,使一次发现结果可以同时服务面向模型的工具、面向用户的命令和受信内部调用方,而不会混淆各自的目录。
|
||||
|
||||
| 策略 | 模型 | 用户 |
|
||||
|---|---|---|
|
||||
| 两个字段均未设置,或分别为 `false` / `true` | 包含 | 包含 |
|
||||
| `userInvocable: false` | 包含 | 排除 |
|
||||
| `disableModelInvocation: true` | 排除 | 包含 |
|
||||
| 两个限制值均已设置 | 排除 | 排除 |
|
||||
| 无 `invocation`,或 `{ modelInvocable: true, userInvocable: true }` | 包含 | 包含 |
|
||||
| `{ modelInvocable: true, userInvocable: false }` | 包含 | 排除 |
|
||||
| `{ modelInvocable: false, userInvocable: true }` | 排除 | 包含 |
|
||||
| `{ modelInvocable: false, userInvocable: false }` | 排除 | 排除 |
|
||||
|
||||
`isModelInvocable(skill)` 仅在 `disableModelInvocation: true` 时返回 false;`isUserInvocable(skill)` 仅在 `userInvocable: false` 时返回 false。`ctx.skills.get()` 仍是受信且与策略无关的加载原语,因此每个面向用户或模型的消费方都必须先执行与自身接口匹配的判定,再暴露或加载 skill。
|
||||
`isModelInvocable(skill)` 和 `isUserInvocable(skill)` 分别读取对应的正向字段;策略缺失时两个接口均允许调用。`ctx.skills.get()` 仍是受信且与策略无关的加载原语,因此每个面向用户或模型的消费方都必须先执行与自身接口匹配的判定,再暴露或加载 skill。
|
||||
|
||||
## 提供方契约
|
||||
|
||||
|
||||
@@ -38,10 +38,10 @@ export type SkillResourceBase =
|
||||
|
||||
/** Invocation controls shared by skill discovery consumers. */
|
||||
export interface SkillInvocationPolicy {
|
||||
/** Whether model-facing catalogs and loaders exclude this skill. */
|
||||
readonly disableModelInvocation?: boolean
|
||||
/** Whether model-facing catalogs and loaders include this skill. */
|
||||
readonly modelInvocable: boolean
|
||||
/** Whether human-facing command catalogs and loaders include this skill. */
|
||||
readonly userInvocable?: boolean
|
||||
readonly userInvocable: boolean
|
||||
}
|
||||
|
||||
/** Invocation-neutral skill metadata returned by `ctx.skills.list()`. */
|
||||
@@ -98,16 +98,16 @@ export interface SkillLookupOptions {
|
||||
/**
|
||||
* Return whether a skill may be advertised to and loaded by a model.
|
||||
* @param skill - skill metadata carrying optional invocation controls.
|
||||
* @returns `false` only when model invocation is explicitly disabled.
|
||||
* @returns whether the normalized policy permits model invocation.
|
||||
*/
|
||||
export function isModelInvocable(skill: Pick<SkillSummary, 'invocation'>): boolean {
|
||||
return skill.invocation?.disableModelInvocation !== true
|
||||
return skill.invocation?.modelInvocable !== false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether a skill may be advertised to and loaded by a human-facing command.
|
||||
* @param skill - skill metadata carrying optional invocation controls.
|
||||
* @returns `false` only when user invocation is explicitly disabled.
|
||||
* @returns whether the normalized policy permits user invocation.
|
||||
*/
|
||||
export function isUserInvocable(skill: Pick<SkillSummary, 'invocation'>): boolean {
|
||||
return skill.invocation?.userInvocable !== false
|
||||
@@ -477,10 +477,10 @@ function validateInvocation(invocation: unknown, subject: string): void {
|
||||
throw new TypeError(`${subject} with a non-object invocation policy`)
|
||||
}
|
||||
const policy = invocation as Record<string, unknown>
|
||||
if (policy.disableModelInvocation !== undefined && typeof policy.disableModelInvocation !== 'boolean') {
|
||||
throw new TypeError(`${subject} with a non-boolean invocation.disableModelInvocation`)
|
||||
if (typeof policy.modelInvocable !== 'boolean') {
|
||||
throw new TypeError(`${subject} with a non-boolean invocation.modelInvocable`)
|
||||
}
|
||||
if (policy.userInvocable !== undefined && typeof policy.userInvocable !== 'boolean') {
|
||||
if (typeof policy.userInvocable !== 'boolean') {
|
||||
throw new TypeError(`${subject} with a non-boolean invocation.userInvocable`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import SkillService, {
|
||||
isUserInvocable,
|
||||
type SkillCandidate,
|
||||
type SkillDefinition,
|
||||
type SkillInvocationPolicy,
|
||||
type SkillLookupOptions,
|
||||
type SkillProvider,
|
||||
} from '@deepseek-ai/dsh-skill'
|
||||
@@ -119,9 +120,9 @@ describe('SkillService registry', () => {
|
||||
await ctx.plugin(SkillService)
|
||||
const registrations = [
|
||||
{ name: 'both', invocation: undefined },
|
||||
{ name: 'model-only', invocation: { userInvocable: false } },
|
||||
{ name: 'user-only', invocation: { disableModelInvocation: true } },
|
||||
{ name: 'trusted-only', invocation: { disableModelInvocation: true, userInvocable: false } },
|
||||
{ name: 'model-only', invocation: { modelInvocable: true, userInvocable: false } },
|
||||
{ name: 'user-only', invocation: { modelInvocable: false, userInvocable: true } },
|
||||
{ name: 'trusted-only', invocation: { modelInvocable: false, userInvocable: false } },
|
||||
] as const
|
||||
for (const registration of registrations) {
|
||||
ctx.skills.register({
|
||||
@@ -150,7 +151,7 @@ describe('SkillService registry', () => {
|
||||
...memorySkill('bad-candidate', 'placeholder', 1),
|
||||
provider: 'bad-candidate',
|
||||
description: badDescription as unknown as string,
|
||||
invocation: { disableModelInvocation: 'false' as unknown as boolean },
|
||||
invocation: { modelInvocable: false, userInvocable: true },
|
||||
}]),
|
||||
get: () => Promise.resolve(undefined),
|
||||
})
|
||||
@@ -163,11 +164,11 @@ describe('SkillService registry', () => {
|
||||
list: () => Promise.resolve([{
|
||||
...memorySkill('bad-boolean', 'Bad boolean', 1),
|
||||
provider: 'bad-boolean',
|
||||
invocation: { disableModelInvocation: 'false' as unknown as boolean },
|
||||
invocation: { modelInvocable: 'false' as unknown as boolean, userInvocable: true },
|
||||
}]),
|
||||
get: () => Promise.resolve(undefined),
|
||||
})
|
||||
await expect(badBoolean.skills.list()).rejects.toThrow('non-boolean invocation.disableModelInvocation')
|
||||
await expect(badBoolean.skills.list()).rejects.toThrow('non-boolean invocation.modelInvocable')
|
||||
})
|
||||
|
||||
it('rejects non-array provider results and every malformed candidate scalar', async () => {
|
||||
@@ -196,7 +197,7 @@ describe('SkillService registry', () => {
|
||||
name: `candidate-${index}`,
|
||||
description: 'Candidate',
|
||||
whenToUse: 'Use this candidate.',
|
||||
invocation: { disableModelInvocation: false, userInvocable: true },
|
||||
invocation: { modelInvocable: true, userInvocable: true },
|
||||
provider: providerName,
|
||||
source: 'test',
|
||||
rank: 1,
|
||||
@@ -353,7 +354,7 @@ describe('SkillService registry', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SkillService)
|
||||
const locator = { id: 'provider-owned' }
|
||||
const invocation = { disableModelInvocation: false, userInvocable: true }
|
||||
const invocation = { modelInvocable: true, userInvocable: true }
|
||||
const candidate: SkillCandidate = {
|
||||
name: 'stable-skill',
|
||||
description: 'Stable description',
|
||||
@@ -414,7 +415,7 @@ describe('SkillService registry', () => {
|
||||
await ctx.plugin(SkillService)
|
||||
const resourceBase = { kind: 'opaque' as const, description: 'runtime resources' }
|
||||
const metadata = { owner: 'runtime' }
|
||||
const invocation = { disableModelInvocation: false, userInvocable: true }
|
||||
const invocation = { modelInvocable: true, userInvocable: true }
|
||||
const registration = {
|
||||
name: 'runtime-skill',
|
||||
description: 'Runtime',
|
||||
@@ -449,11 +450,19 @@ describe('SkillService registry', () => {
|
||||
{ patch: { description: '' }, expected: 'requires a description' },
|
||||
{ patch: { invocation: null as never }, expected: 'non-object invocation policy' },
|
||||
{
|
||||
patch: { invocation: { disableModelInvocation: 'false' as unknown as boolean } },
|
||||
expected: 'invocation.disableModelInvocation',
|
||||
patch: { invocation: { modelInvocable: 'false' as unknown as boolean, userInvocable: true } },
|
||||
expected: 'invocation.modelInvocable',
|
||||
},
|
||||
{
|
||||
patch: { invocation: { userInvocable: 'true' as unknown as boolean } },
|
||||
patch: { invocation: { modelInvocable: true, userInvocable: 'true' as unknown as boolean } },
|
||||
expected: 'invocation.userInvocable',
|
||||
},
|
||||
{
|
||||
patch: { invocation: { userInvocable: true } as unknown as SkillInvocationPolicy },
|
||||
expected: 'invocation.modelInvocable',
|
||||
},
|
||||
{
|
||||
patch: { invocation: { modelInvocable: true } as unknown as SkillInvocationPolicy },
|
||||
expected: 'invocation.userInvocable',
|
||||
},
|
||||
{ patch: { whenToUse: 1 as unknown as string }, expected: 'whenToUse must be a string' },
|
||||
@@ -481,7 +490,7 @@ describe('SkillService registry', () => {
|
||||
name: skillName,
|
||||
description: 'Definition',
|
||||
whenToUse: 'Use this definition.',
|
||||
invocation: { disableModelInvocation: false, userInvocable: true },
|
||||
invocation: { modelInvocable: true, userInvocable: true },
|
||||
provider: providerName,
|
||||
source: 'test',
|
||||
content: 'Definition body.',
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/skill/tool-skill/README.md
|
||||
README.md: 37cda665b74e186d26129c74401b4d8b24c9b7c3
|
||||
README.zh.md: 849e44513c69ed11c311a1437c8a9a4af03a03f0
|
||||
README.md: ddf2b7503e8dad47677aa5260b0aa3349f4c4dd1
|
||||
README.zh.md: 80255f0c1f78842b848a89b0a322b677a1364641
|
||||
|
||||
@@ -22,7 +22,7 @@ Execution uses the calling agent's `session.header.cwd` so workspace-sensitive p
|
||||
|
||||
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.
|
||||
|
||||
An unresolved name reports that the skill is unknown or no longer available. Invalid names and skills whose `invocation.disableModelInvocation` is `true` produce distinct error results. `invocation.userInvocable` does not restrict this model-facing surface.
|
||||
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.
|
||||
|
||||
The tool does not call `agent.inject()` in v1. Its result is already recorded as the tool result and becomes available to the next model step without duplicating the content as synthetic context.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
资源指引只会根据 `resourceBase` 解析指令显式引用的路径或 URL;脚本、参考资料和产物按需加载,结果不会列举 skill 目录。本地提供方可以提供目录,而远程或嵌入式提供方可以提供 URL 或不透明加载指引。
|
||||
|
||||
无法解析的名称会报告 skill 未知或已不可用。无效名称和 `invocation.disableModelInvocation` 为 `true` 的 skill 会产生不同的错误结果。`invocation.userInvocable` 不限制这个面向模型的接口。
|
||||
无法解析的名称会报告 skill 未知或已不可用。无效名称和 `invocation.modelInvocable` 为 `false` 的 skill 会产生不同的错误结果。`invocation.userInvocable` 不限制这个面向模型的接口。
|
||||
|
||||
该工具在 v1 中不调用 `agent.inject()`。其结果已作为工具结果记录,并在下一个模型步骤可用,无需将内容重复为合成上下文。
|
||||
|
||||
|
||||
@@ -146,14 +146,14 @@ describe('dsh-tool-skill', () => {
|
||||
ctx.skills.register({
|
||||
name: 'model-only-skill',
|
||||
description: 'Model-only skill.',
|
||||
invocation: { userInvocable: false },
|
||||
invocation: { modelInvocable: true, userInvocable: false },
|
||||
source: 'runtime',
|
||||
content: 'Model-only body.',
|
||||
})
|
||||
ctx.skills.register({
|
||||
name: 'user-only-skill',
|
||||
description: 'User-only skill.',
|
||||
invocation: { disableModelInvocation: true },
|
||||
invocation: { modelInvocable: false, userInvocable: true },
|
||||
source: 'runtime',
|
||||
content: 'User-only body.',
|
||||
})
|
||||
@@ -200,7 +200,7 @@ describe('dsh-tool-skill', () => {
|
||||
ctx.skills.register({
|
||||
name: 'user-only-skill',
|
||||
description: 'User-only skill',
|
||||
invocation: { disableModelInvocation: true },
|
||||
invocation: { modelInvocable: false, userInvocable: true },
|
||||
source: 'runtime',
|
||||
content: 'User-only body.',
|
||||
})
|
||||
@@ -361,7 +361,7 @@ describe('dsh-tool-skill', () => {
|
||||
ctx.skills.register({
|
||||
name: 'model-only-skill',
|
||||
description: 'Model-only skill',
|
||||
invocation: { userInvocable: false },
|
||||
invocation: { modelInvocable: true, userInvocable: false },
|
||||
source: 'runtime',
|
||||
content: 'Model-only instructions.',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user