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/ui/tui/README.md
|
||||
README.md: 76d7f09d8b69123705f0d30c203b01989afe605c
|
||||
README.zh.md: 61fe34fc09ffea6d81442bcd23c99a340c1e0ac0
|
||||
README.md: 126f39f37ed86d049da53facea5379841a314ad8
|
||||
README.zh.md: 1dc7b925a0cd239f003e209f73841afd049d4dc9
|
||||
|
||||
@@ -130,7 +130,7 @@ Changing provider or model enters that target's cache domain; no cache reuse acr
|
||||
|
||||
#### What the model sees
|
||||
|
||||
A `/skill:<name> [instructions]` submission loads the named skill and delivers one text block: a `<skill name="…">` element wrapping the skill's instructions — preceded, when the provider exposes a resource base, by a line locating the skill's relative resources — followed by any trailing instructions the user typed. Delivery follows the same followup-while-idle / steer-while-running rule as ordinary input. The command, not the model, chooses the skill: autocomplete and exact invocation apply `userInvocable`, while `disableModelInvocation` does not restrict this surface. A user-disabled skill produces a warning and no user turn.
|
||||
A `/skill:<name> [instructions]` submission loads the named skill and delivers one text block: a `<skill name="…">` element wrapping the skill's instructions — preceded, when the provider exposes a resource base, by a line locating the skill's relative resources — followed by any trailing instructions the user typed. Delivery follows the same followup-while-idle / steer-while-running rule as ordinary input. The command, not the model, chooses the skill: autocomplete and exact invocation apply `invocation.userInvocable`, while `invocation.modelInvocable` does not restrict this surface. A user-disabled skill produces a warning and no user turn. The skill service is an optional peer; this policy check uses its type contract without introducing a runtime package dependency.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ Paths prefixed with @ are files explicitly referenced by the user. Use the read
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
提交 `/skill:<name> [instructions]` 会加载具名 skill,并交付一个文本块:用 `<skill name="…">` 元素包装 skill 指令;提供方公开资源基准时,会先添加一行定位 skill 相对资源;最后附上用户输入的尾随指令。交付遵循普通输入同样的空闲时 followup、运行时 steer 规则。选择 skill 的是命令而非模型:自动补全和按精确名称调用都应用 `userInvocable`,`disableModelInvocation` 不限制这个接口。用户禁用的 skill 会产生一条警告,不会产生用户轮次。
|
||||
提交 `/skill:<name> [instructions]` 会加载具名 skill,并交付一个文本块:用 `<skill name="…">` 元素包装 skill 指令;提供方公开资源基准时,会先添加一行定位 skill 相对资源;最后附上用户输入的尾随指令。交付遵循普通输入同样的空闲时 followup、运行时 steer 规则。选择 skill 的是命令而非模型:自动补全和按精确名称调用都应用 `invocation.userInvocable`,`invocation.modelInvocable` 不限制这个接口。用户禁用的 skill 会产生一条警告,不会产生用户轮次。skill 服务是可选 peer;这项策略检查仅使用其类型契约,不引入运行时包依赖。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
|
||||
@@ -49,10 +49,7 @@ import { foldSessionTitle } from '@deepseek-ai/dsh-session-title'
|
||||
// Type import also declaration-merges the optional `sessionPersistence`
|
||||
// service onto `Context` so `ctx.get('sessionPersistence')` is typed.
|
||||
import type {} from '@deepseek-ai/dsh-session-persistence'
|
||||
import {
|
||||
isUserInvocable,
|
||||
type SkillService,
|
||||
} from '@deepseek-ai/dsh-skill'
|
||||
import type { SkillService, SkillSummary } from '@deepseek-ai/dsh-skill'
|
||||
// Type import declaration-merges the `userInteraction` service onto `Context`;
|
||||
// the ask-user-question queue is registered by ./chat/questions.
|
||||
import type {} from '@deepseek-ai/dsh-user-interaction'
|
||||
@@ -173,6 +170,10 @@ export type {
|
||||
TuiViewport,
|
||||
} from './extension/types.ts'
|
||||
|
||||
function isSkillUserInvocable(skill: Pick<SkillSummary, 'invocation'>): boolean {
|
||||
return skill.invocation?.userInvocable !== false
|
||||
}
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
/** Terminal-only interaction service, available only while a TUI is mounted. */
|
||||
@@ -1000,7 +1001,7 @@ export function createTuiChat(
|
||||
const loadSkillCommands = (service: SkillService): void => {
|
||||
service.list({ cwd, signal: skillAbort.signal }).then(
|
||||
(summaries) => {
|
||||
const invocable = summaries.filter(isUserInvocable)
|
||||
const invocable = summaries.filter(isSkillUserInvocable)
|
||||
if (disposed || invocable.length === 0) return
|
||||
// The argument-hint slot shows in the menu but is never inserted on
|
||||
// selection, so it carries the skill's scope instead of an
|
||||
@@ -1213,7 +1214,7 @@ export function createTuiChat(
|
||||
appendNotice(`Unknown skill: ${name}`, 'warning')
|
||||
return
|
||||
}
|
||||
if (!isUserInvocable(skill)) {
|
||||
if (!isSkillUserInvocable(skill)) {
|
||||
appendNotice(`Skill "${name}" is not available for user invocation.`, 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3547,21 +3547,21 @@ describe('skill slash command', () => {
|
||||
skills.register({
|
||||
name: 'user-only-skill',
|
||||
description: 'User-only skill',
|
||||
invocation: { disableModelInvocation: true },
|
||||
invocation: { modelInvocable: false, userInvocable: true },
|
||||
source: 'runtime',
|
||||
content: 'User-only instructions body.',
|
||||
})
|
||||
skills.register({
|
||||
name: 'model-only-skill',
|
||||
description: 'Model-only skill',
|
||||
invocation: { userInvocable: false },
|
||||
invocation: { modelInvocable: true, userInvocable: false },
|
||||
source: 'runtime',
|
||||
content: 'Model-only instructions body.',
|
||||
})
|
||||
skills.register({
|
||||
name: 'trusted-only-skill',
|
||||
description: 'Trusted-only skill',
|
||||
invocation: { disableModelInvocation: true, userInvocable: false },
|
||||
invocation: { modelInvocable: false, userInvocable: false },
|
||||
source: 'runtime',
|
||||
content: 'Trusted-only instructions body.',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user