Merge origin/master into worktree/skill-catalog-hot-refresh

This commit is contained in:
Tianyi Cui
2026-07-29 16:40:40 +08:00
1089 changed files with 35627 additions and 9637 deletions

View File

@@ -9,7 +9,8 @@ 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 { createUserMessage, assertNever } from '@deepseek-ai/dsh-llm'
import type { UserMessage } from '@deepseek-ai/dsh-session'
import { isSkillName, type SkillDefinition, type SkillSummary } from '@deepseek-ai/dsh-skill'
export const name = 'tool-skill'
@@ -18,7 +19,7 @@ export const inject = ['agents', 'tools', 'skills']
const DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH = 500
const CATALOG_ENTRIES_START = '<available_skills>\n'
const CATALOG_ENTRIES_END = '\n</available_skills>'
const PLUGIN_SOURCE = { kind: 'plugin', plugin: name } as const
const PLUGIN_SOURCE = { kind: 'plugin', plugin: 'dsh-tool-skill' } as const
/** Model-facing skill catalog configuration. */
export interface Config {
@@ -134,10 +135,7 @@ export function apply(ctx: Context, config: Config = {}): void {
const catalog = history.published
? renderCatalogUpdate(snapshot.skills, catalogDescriptionMaxLength)
: renderCatalogMessage(snapshot.skills, catalogDescriptionMaxLength)
agent.inject({
content: catalog.content,
source: PLUGIN_SOURCE,
})
agent.inject(catalog)
})
}
@@ -187,10 +185,9 @@ function renderResourceHint(skill: Pick<SkillDefinition, 'provider' | 'resourceB
}
}
function renderCatalogMessage(skills: SkillSummary[], descriptionMaxLength: number): Message {
function renderCatalogMessage(skills: SkillSummary[], descriptionMaxLength: number): UserMessage {
const entries = renderCatalogEntries(skills, descriptionMaxLength)
return {
role: 'user',
return createUserMessage({
content: [{
type: 'text',
text: [
@@ -205,10 +202,11 @@ function renderCatalogMessage(skills: SkillSummary[], descriptionMaxLength: numb
'</system-reminder>',
].join('\n'),
}],
}
source: PLUGIN_SOURCE,
})
}
function renderCatalogUpdate(skills: SkillSummary[], descriptionMaxLength: number): Message {
function renderCatalogUpdate(skills: SkillSummary[], descriptionMaxLength: number): UserMessage {
const entries = renderCatalogEntries(skills, descriptionMaxLength)
const availability = skills.length === 0
? [
@@ -217,8 +215,7 @@ function renderCatalogUpdate(skills: SkillSummary[], descriptionMaxLength: numbe
: [
'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.',
]
return {
role: 'user',
return createUserMessage({
content: [{
type: 'text',
text: [
@@ -233,7 +230,8 @@ function renderCatalogUpdate(skills: SkillSummary[], descriptionMaxLength: numbe
'</system-reminder>',
].join('\n'),
}],
}
source: PLUGIN_SOURCE,
})
}
function renderCatalogEntries(skills: SkillSummary[], descriptionMaxLength: number): string[] {
@@ -260,7 +258,7 @@ function catalogHistory(agent: Agent): { visibleDigest?: string; published: bool
const event = events[index]!
if (event.type !== 'user/message'
|| event.data.source.kind !== 'plugin'
|| event.data.source.plugin !== name) continue
|| event.data.source.plugin !== PLUGIN_SOURCE.plugin) continue
const digest = catalogContentDigest(event.data.content)
if (digest === undefined) continue
published = true
@@ -269,7 +267,7 @@ function catalogHistory(agent: Agent): { visibleDigest?: string; published: bool
return { published }
}
function catalogContentDigest(content: Message['content']): string | undefined {
function catalogContentDigest(content: UserMessage['content']): string | undefined {
if (content.length !== 1 || content[0]?.type !== 'text') return undefined
const text = content[0].text
const start = text.indexOf(CATALOG_ENTRIES_START)

View File

@@ -3,12 +3,12 @@ import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { Context } from 'cordis'
import { CallId, type Message } from '@deepseek-ai/dsh-llm'
import { createUserMessage, CallId, type Message } from '@deepseek-ai/dsh-llm'
import { createScope, type Scope } from '@deepseek-ai/dsh-scope'
import { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { agentEvents, AgentMessageId, type Agent } from '@deepseek-ai/dsh-agent'
import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import SkillService from '@deepseek-ai/dsh-skill'
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
import * as toolSkill from '@deepseek-ai/dsh-tool-skill'
@@ -46,12 +46,11 @@ function agentForCwd(cwd: string): Agent {
session,
status: 'idle',
acceptsNextStep: false,
send: () => AgentMessageId('stub'),
followup: () => AgentMessageId('stub'),
steer: () => AgentMessageId('stub'),
send: () => {},
followup: () => {},
steer: () => {},
inject(input) {
session.append('user/message', input, { surfaceOp: 'append' })
return AgentMessageId('stub')
},
cancel() {},
whenIdle: () => Promise.resolve(),
@@ -66,12 +65,11 @@ function sessionAgent(session: Session, id = 'tool-skill-agent'): Agent {
status: 'running',
acceptsNextStep: false,
ctx: new Context(),
send: () => AgentMessageId('stub'),
followup: () => AgentMessageId('stub'),
steer: () => AgentMessageId('stub'),
send: () => {},
followup: () => {},
steer: () => {},
inject(input) {
session.append('user/message', input, { surfaceOp: 'append' })
return AgentMessageId('stub')
},
cancel() {},
whenIdle: () => Promise.resolve(),
@@ -80,10 +78,10 @@ function sessionAgent(session: Session, id = 'tool-skill-agent'): Agent {
function openMessageTurn(session: Session, turn = 1): void {
session.append('turn/start', { turn, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: `turn ${turn}` }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
}
async function fireStep(ctx: Context, agent: Agent, turn: number, step: number): Promise<void> {
@@ -93,7 +91,7 @@ async function fireStep(ctx: Context, agent: Agent, turn: number, step: number):
function catalogMessages(session: Session): Extract<SessionEvent, { type: 'user/message' }>[] {
return session.events.filter((event): event is Extract<SessionEvent, { type: 'user/message' }> => event.type === 'user/message'
&& event.data.source.kind === 'plugin'
&& event.data.source.plugin === 'tool-skill')
&& event.data.source.plugin === 'dsh-tool-skill')
}
function catalogContent(entries: string[]): Message['content'] {
@@ -190,14 +188,16 @@ describe('dsh-tool-skill', () => {
content: 'A body.',
})
ctx.on('agent/step', (agent) => {
agent.inject({ content: [{ type: 'text', text: 'later contribution' }], source: { kind: 'plugin', plugin: 'later-contribution' } })
agent.inject(createUserMessage({ content: [{ type: 'text', text: 'later contribution' }], source: { kind: 'plugin', plugin: 'later-contribution' } }))
})
const prefix = await composePrefix(ctx, '/workspace')
expect(prefix).toEqual([
{
id: expect.any(String) as unknown,
role: 'user',
source: { kind: 'plugin', plugin: 'dsh-tool-skill' },
content: [{
type: 'text',
text: [
@@ -214,7 +214,12 @@ describe('dsh-tool-skill', () => {
].join('\n'),
}],
},
{ role: 'user', content: [{ type: 'text', text: 'later contribution' }] },
{
id: expect.any(String) as unknown,
role: 'user',
content: [{ type: 'text', text: 'later contribution' }],
source: { kind: 'plugin', plugin: 'later-contribution' },
},
])
const rendered = JSON.stringify(prefix[0])
expect(rendered).not.toContain('whenToUse')
@@ -329,26 +334,26 @@ describe('dsh-tool-skill', () => {
const session = new Session(SessionId('catalog-resume'))
const agent = sessionAgent(session)
openMessageTurn(session)
session.append('user/message', {
session.append('user/message', createUserMessage({
content: catalogContent(['- `old-skill`: Old skill']),
source: { kind: 'plugin', plugin: 'tool-skill' },
}, { surfaceOp: 'append' })
session.append('user/message', {
source: { kind: 'plugin', plugin: 'dsh-tool-skill' },
}), { surfaceOp: 'append' })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'missing catalog markers' }],
source: { kind: 'plugin', plugin: 'tool-skill' },
}, { surfaceOp: 'append' })
session.append('user/message', {
source: { kind: 'plugin', plugin: 'dsh-tool-skill' },
}), { surfaceOp: 'append' })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: '<available_skills>\nmissing closing marker' }],
source: { kind: 'plugin', plugin: 'tool-skill' },
}, { surfaceOp: 'append' })
session.append('user/message', {
source: { kind: 'plugin', plugin: 'dsh-tool-skill' },
}), { surfaceOp: 'append' })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'first block' }, { type: 'text', text: 'second block' }],
source: { kind: 'plugin', plugin: 'tool-skill' },
}, { surfaceOp: 'append' })
session.append('user/message', {
source: { kind: 'plugin', plugin: 'dsh-tool-skill' },
}), { surfaceOp: 'append' })
session.append('user/message', createUserMessage({
content: [{ type: 'reasoning', text: 'not a user-role catalog block' }],
source: { kind: 'plugin', plugin: 'tool-skill' },
}, { surfaceOp: 'append' })
source: { kind: 'plugin', plugin: 'dsh-tool-skill' },
}), { surfaceOp: 'append' })
await fireStep(ctx, agent, 1, 1)
@@ -371,10 +376,10 @@ describe('dsh-tool-skill', () => {
expect(JSON.stringify(await composePrefixForAgent(ctx, agent))).toContain('first-skill')
const initial = catalogMessages(session)[0]
if (initial === undefined) throw new Error('expected initial catalog')
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'compacted history' }],
source: { kind: 'plugin', plugin: 'compact' },
}, {
}), {
surfaceOp: { op: 'replace', start: initial.seq, end: initial.seq },
sourceEventSeqs: [initial.seq],
})