Merge origin/master into feat/tui-master-port
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
|
||||
README.md: 50a0e06ac06ca8a3b89c3d5ac604dcf2dc423533
|
||||
README.zh.md: 56fb2b87adcf072cf2b8b6670864fa274ed5f66f
|
||||
README.md: 6b0af04a15bfda985be3e04868f18b9af702eae7
|
||||
README.zh.md: 4ca0ccd7734848d5774e92910e916bdf71c88c13
|
||||
|
||||
@@ -6,11 +6,11 @@ The model-facing skill catalog and `skill` tool.
|
||||
|
||||
Requires `ctx.tools` and `ctx.skills` (`inject: ['tools', 'skills']`).
|
||||
|
||||
## Session-prefix catalog
|
||||
## Session catalog
|
||||
|
||||
The plugin contributes one user-role `<system-reminder>` catalog through `agent/session-prefix`. It resolves skills for the calling session's cwd, forwards the prefix abort signal to discovery, and lists only sorted `name` and `description` entries; skill bodies, paths, sources, providers, and `whenToUse` hints remain outside the catalog. The catalog is omitted when no model-invocable skills are available, and also when that agent's tool view restricts away the shipped `skill` tool or resolves a same-name scoped shadow instead. This exact-definition check keeps prompt guidance, the model-visible schema, and executable dispatch aligned.
|
||||
The plugin injects one durable user-role `<system-reminder>` catalog at the first `agent/step` of a live session. It resolves skills for the calling session's cwd, forwards the step abort signal to discovery, and lists only sorted `name` and `description` entries; skill bodies, paths, sources, providers, and `whenToUse` hints remain outside the catalog. The catalog is omitted when no model-invocable skills are available, and also when that agent's tool view restricts away the shipped `skill` tool or resolves a same-name scoped shadow instead. This exact-definition check keeps prompt guidance, the model-visible schema, and executable dispatch aligned.
|
||||
|
||||
`catalogDescriptionMaxLength` controls normalized, XML-escaped catalog descriptions. Its default is `500` and values must be integers of at least `3`, which reserves room for a truncation ellipsis. The [session-prefix Agent Note](../../../.agents/notes/implemented/feature/2026-07-07-session-prefix.md) defines the request-only, header-logged lifecycle of this message.
|
||||
`catalogDescriptionMaxLength` controls normalized, XML-escaped catalog descriptions. Its default is `500` and values must be integers of at least `3`, which reserves room for a truncation ellipsis. The catalog is a sourced `user/message` injected before the first request and retained in ordinary session history.
|
||||
|
||||
## Tool: `skill`
|
||||
|
||||
@@ -28,11 +28,11 @@ The tool does not call `agent.inject()` in v1. Its result is already recorded as
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Session prefix
|
||||
### Session catalog
|
||||
|
||||
#### What the model sees
|
||||
|
||||
If model-invocable skills exist and this exact `skill` tool is visible, the agent receives the catalog template below, with one data-dependent entry per sorted skill. The catalog is a frozen user-role session prefix.
|
||||
If model-invocable skills exist and this exact `skill` tool is visible, the agent receives the catalog template below, with one data-dependent entry per sorted skill. The catalog is one durable user-role message.
|
||||
|
||||
##### Skill catalog template
|
||||
|
||||
@@ -54,7 +54,7 @@ Repeated input cost scales with skill count and `catalogDescriptionMaxLength`; n
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Prefix-stable within a loop instance once the session prefix is composed. A new or resumed instance with different providers, skills, descriptions, visibility, or catalog limits may invalidate reuse from the first changed catalog token.
|
||||
Append-only after the existing reusable prefix. A new or resumed instance with different providers, skills, descriptions, visibility, or catalog limits may affect cache reuse from the newly appended catalog position.
|
||||
|
||||
### Tool schema
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
需要 `ctx.tools` 和 `ctx.skills` (`inject: ['tools', 'skills']`)。
|
||||
|
||||
## 会话前缀目录
|
||||
## 会话目录
|
||||
|
||||
该插件贡献一个用户角色 `<system-reminder>` 目录,并通过 `agent/session-prefix` 提供它。它为调用会话的 cwd 解析 skill,将前缀中止信号转发到发现,并只列出已排序的 `name` 和 `description` 条目;skill 正文、路径、来源、提供方和 `whenToUse` 提示仍位于目录之外。如果没有模型可调用 skill,则省略目录;如果该 agent 的工具视图排除已发布的 `skill` 工具,或解析出一个同名作用域遮蔽,也会省略目录。这项精确定义检查使提示词指引、模型可见 schema 和可执行分派保持对齐。
|
||||
该插件在实时会话的第一个 `agent/step` 注入一条持久的用户角色 `<system-reminder>` 目录。它为调用会话的 cwd 解析 skill,将步骤中止信号转发到发现,并只列出已排序的 `name` 和 `description` 条目;skill 正文、路径、来源、提供方和 `whenToUse` 提示仍位于目录之外。如果没有模型可调用 skill,则省略目录;如果该 agent 的工具视图排除已发布的 `skill` 工具,或解析出一个同名作用域遮蔽,也会省略目录。这项精确定义检查使提示词指引、模型可见 schema 和可执行分派保持对齐。
|
||||
|
||||
`catalogDescriptionMaxLength` 控制规范化且经 XML 转义的目录描述。其默认值是 `500`,且必须是不小于 `3` 的整数,以便为截断省略号保留空间。[会话前缀 Agent Note](../../../.agents/notes/implemented/feature/2026-07-07-session-prefix.md) 定义了该消息仅存在于请求中、记录于 header 的生命周期。
|
||||
`catalogDescriptionMaxLength` 控制规范化且经 XML 转义的目录描述。其默认值是 `500`,且必须是不小于 `3` 的整数,以便为截断省略号保留空间。目录是一条带来源的 `user/message`,在第一个请求前注入,并保留在普通会话历史中。
|
||||
|
||||
## 工具:`skill`
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
|
||||
## 模型体验
|
||||
|
||||
### 会话前缀
|
||||
### 会话目录
|
||||
|
||||
#### 模型所见
|
||||
|
||||
如果存在模型可调用 skill,且该精确 `skill` 工具可见,agent 会收到下方目录模板,其中包含每个已排序 skill 的一条数据依赖条目。该目录是冻结的用户角色会话前缀。
|
||||
如果存在模型可调用 skill,且该精确 `skill` 工具可见,agent 会收到下方目录模板,其中包含每个已排序 skill 的一条数据依赖条目。该目录是一条持久的用户角色消息。
|
||||
|
||||
##### Skill 目录模板
|
||||
|
||||
@@ -54,7 +54,7 @@ If the user names a skill, or the task clearly matches a skill's description, ca
|
||||
|
||||
#### KV 缓存影响
|
||||
|
||||
会话前缀组合完成后,在一个循环实例内前缀稳定。如果新建或恢复的实例具有不同提供方、skill、描述、可见性或目录上限,则可能从第一个变更目录 token 起使重用失效。
|
||||
仅追加,位于现有可重用前缀之后。如果新建或恢复的实例具有不同提供方、skill、描述、可见性或目录上限,则可能从新追加的目录位置起影响缓存重用。
|
||||
|
||||
### 工具 schema
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* Session-prefix skill catalog and model-facing `skill` loader tool.
|
||||
* Durable session skill catalog and model-facing `skill` loader tool.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-tool-skill
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import { assertNever, type Message } from '@deepseek-ai/dsh-llm'
|
||||
import { isSkillName, type SkillDefinition, type SkillSummary } from '@deepseek-ai/dsh-skill'
|
||||
@@ -28,7 +29,7 @@ export const Config: z<Config> = z.object({
|
||||
|
||||
/**
|
||||
* Register the model-facing skill loader and its visibility-matched
|
||||
* session-prefix catalog. The catalog is emitted only when the calling agent
|
||||
* durable session catalog. The catalog is emitted only when the calling agent
|
||||
* resolves this plugin's exact tool registration; a restriction or scoped
|
||||
* same-name shadow therefore removes both the schema and its call guidance.
|
||||
*/
|
||||
@@ -115,12 +116,19 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
|
||||
// Register after the tool so reverse teardown removes guidance first. Exact definition
|
||||
// identity prevents a scoped shadow merely named `skill` from inheriting this catalog.
|
||||
ctx.on('agent/session-prefix', async (agent, _prefix, signal, next): Promise<Message[]> => {
|
||||
if (ctx.tools.get(skillTool.name, agent) !== registeredSkillTool) return await next()
|
||||
const catalogLoaded = new WeakSet<object>()
|
||||
ctx.on('agent/step', async (agent: Agent, _turn, _step, signal): Promise<void> => {
|
||||
if (catalogLoaded.has(agent.session)) return
|
||||
if (ctx.tools.get(skillTool.name, agent) !== registeredSkillTool) {
|
||||
catalogLoaded.add(agent.session)
|
||||
return
|
||||
}
|
||||
const skills = await ctx.skills.list({ cwd: agent.session.header.cwd, signal })
|
||||
const rest = await next()
|
||||
if (skills.length === 0) return rest
|
||||
return [renderCatalogMessage(skills, catalogDescriptionMaxLength), ...rest]
|
||||
if (skills.length > 0) {
|
||||
const catalog = renderCatalogMessage(skills, catalogDescriptionMaxLength)
|
||||
agent.inject({ content: catalog.content, source: { kind: 'plugin', plugin: 'dsh-tool-skill' } })
|
||||
}
|
||||
catalogLoaded.add(agent.session)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { Context } from 'cordis'
|
||||
import { CallId, type Message } from '@deepseek-ai/dsh-llm'
|
||||
import { AgentMessageId } from '@deepseek-ai/dsh-agent'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { createScope, type Scope } from '@deepseek-ai/dsh-scope'
|
||||
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
@@ -35,7 +37,25 @@ async function setup(home: string, config: toolSkill.Config = {}): Promise<Conte
|
||||
}
|
||||
|
||||
function agentForCwd(cwd: string): Agent {
|
||||
return { session: { header: { cwd } } } as unknown as Agent
|
||||
const id = SessionId(`tool-skill-${cwd}`)
|
||||
const session = new Session(id, [], { version: 0, id, createdAt: 0, cwd })
|
||||
return {
|
||||
ctx: new Context(),
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
status: 'idle',
|
||||
acceptsNextStep: false,
|
||||
send: () => AgentMessageId('stub'),
|
||||
followup: () => AgentMessageId('stub'),
|
||||
steer: () => AgentMessageId('stub'),
|
||||
inject(input) {
|
||||
session.append('user/message', input, { surfaceOp: 'append' })
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
cancel() {},
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
}
|
||||
|
||||
async function composePrefix(ctx: Context, cwd: string, signal = new AbortController().signal): Promise<Message[]> {
|
||||
@@ -43,11 +63,8 @@ async function composePrefix(ctx: Context, cwd: string, signal = new AbortContro
|
||||
}
|
||||
|
||||
async function composePrefixForAgent(ctx: Context, agent: Agent, signal = new AbortController().signal): Promise<Message[]> {
|
||||
const empty: Message[] = []
|
||||
return await agentEvents(ctx, agent).waterfall(
|
||||
'agent/session-prefix', empty, signal,
|
||||
() => Promise.resolve(empty),
|
||||
)
|
||||
await agentEvents(ctx, agent).serial('agent/step', 1, 1, signal)
|
||||
return agent.session.deriveMessages()
|
||||
}
|
||||
|
||||
async function mintAgentScope(ctx: Context, cwd: string): Promise<{ agent: Agent; scope: Scope }> {
|
||||
@@ -86,7 +103,7 @@ describe('dsh-tool-skill', () => {
|
||||
expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(['skill'])
|
||||
})
|
||||
|
||||
it('forwards the session-prefix abort signal to skill discovery', async () => {
|
||||
it('forwards the step abort signal to skill discovery', async () => {
|
||||
const home = await tempDir('tool-prefix-signal')
|
||||
const ctx = await setup(home)
|
||||
let seenSignal: AbortSignal | undefined
|
||||
@@ -107,7 +124,7 @@ describe('dsh-tool-skill', () => {
|
||||
expect(seenSignal).toBe(controller.signal)
|
||||
})
|
||||
|
||||
it('contributes a stable name-and-description catalog through the session prefix', async () => {
|
||||
it('injects a stable durable name-and-description catalog at the first step', async () => {
|
||||
const home = await tempDir('tool-catalog')
|
||||
const ctx = await setup(home, { catalogDescriptionMaxLength: 50 })
|
||||
ctx.skills.register({
|
||||
@@ -126,10 +143,9 @@ describe('dsh-tool-skill', () => {
|
||||
provider: 'runtime',
|
||||
content: 'A body.',
|
||||
})
|
||||
ctx.on('agent/session-prefix', async (_agent, _prefix, _signal, next): Promise<Message[]> => [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'later contribution' }] },
|
||||
...await next(),
|
||||
])
|
||||
ctx.on('agent/step', (agent) => {
|
||||
agent.inject({ content: [{ type: 'text', text: 'later contribution' }], source: { kind: 'plugin', plugin: 'later-contribution' } })
|
||||
})
|
||||
|
||||
const prefix = await composePrefix(ctx, '/workspace')
|
||||
|
||||
@@ -162,7 +178,7 @@ describe('dsh-tool-skill', () => {
|
||||
expect(renderPrompt(await ctx.systemPrompt.assemble({ agent: agentForCwd('/workspace') }))).not.toContain('<available_skills>')
|
||||
})
|
||||
|
||||
it('does not contribute a session-prefix message when no skills are available', async () => {
|
||||
it('does not inject a catalog when no skills are available', async () => {
|
||||
const home = await tempDir('tool-empty-catalog')
|
||||
const ctx = await setup(home)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user