Merge origin/master into feat/tui-master-port

This commit is contained in:
Tianyi Cui
2026-07-27 23:34:56 +08:00
713 changed files with 16615 additions and 13373 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)
})
}

View File

@@ -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)