|
|
@@ -4,8 +4,9 @@ import { join } from 'node:path'
|
|
|
import { tmpdir } from 'node:os'
|
|
|
import { Context } from 'cordis'
|
|
|
import { CallId, type Message } from '@deepseek-ai/dsh-llm'
|
|
|
+import { createScope, type Scope } from '@deepseek-ai/dsh-scope'
|
|
|
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
|
|
-import ToolRegistry from '@deepseek-ai/dsh-tools'
|
|
|
+import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
|
|
import SkillService from '@deepseek-ai/dsh-skill'
|
|
|
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
|
|
|
@@ -36,7 +37,10 @@ function agentForCwd(cwd: string): Agent {
|
|
|
}
|
|
|
|
|
|
async function composePrefix(ctx: Context, cwd: string, signal = new AbortController().signal): Promise<Message[]> {
|
|
|
- const agent = agentForCwd(cwd)
|
|
|
+ return await composePrefixForAgent(ctx, agentForCwd(cwd), signal)
|
|
|
+}
|
|
|
+
|
|
|
+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,
|
|
|
@@ -44,6 +48,15 @@ async function composePrefix(ctx: Context, cwd: string, signal = new AbortContro
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+async function mintAgentScope(ctx: Context, cwd: string): Promise<{ agent: Agent; scope: Scope }> {
|
|
|
+ const agent = agentForCwd(cwd)
|
|
|
+ let scope!: Scope
|
|
|
+ await ctx.plugin(Object.assign((inner: Context) => { scope = createScope(inner, agent) }, {
|
|
|
+ inject: ['tools'],
|
|
|
+ }))
|
|
|
+ return { agent, scope }
|
|
|
+}
|
|
|
+
|
|
|
describe('dsh-tool-skill', () => {
|
|
|
it('registers the skill tool schema and removes it on dispose', async () => {
|
|
|
const ctx = new Context()
|
|
|
@@ -154,6 +167,39 @@ describe('dsh-tool-skill', () => {
|
|
|
expect(await composePrefix(ctx, '/workspace')).toEqual([])
|
|
|
})
|
|
|
|
|
|
+ it('omits catalog guidance when the calling agent restricts away the shipped skill tool', async () => {
|
|
|
+ const home = await tempDir('tool-restricted-catalog')
|
|
|
+ const ctx = await setup(home)
|
|
|
+ ctx.skills.register({ name: 'listed-skill', description: 'Listed', source: 'runtime', content: 'body' })
|
|
|
+ const { agent, scope } = await mintAgentScope(ctx, '/workspace')
|
|
|
+ scope.ctx.tools.restrict({ deny: ['skill'] })
|
|
|
+
|
|
|
+ expect(ctx.tools.get('skill', agent)).toBeUndefined()
|
|
|
+ expect(await composePrefixForAgent(ctx, agent)).toEqual([])
|
|
|
+ expect(await composePrefix(ctx, '/workspace')).toHaveLength(1)
|
|
|
+ await scope.dispose()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('does not attach shipped catalog guidance to a scoped same-name tool shadow', async () => {
|
|
|
+ const home = await tempDir('tool-shadowed-catalog')
|
|
|
+ const ctx = await setup(home)
|
|
|
+ ctx.skills.register({ name: 'listed-skill', description: 'Listed', source: 'runtime', content: 'body' })
|
|
|
+ const { agent, scope } = await mintAgentScope(ctx, '/workspace')
|
|
|
+ scope.ctx.tools.register(defineTool({
|
|
|
+ name: 'skill',
|
|
|
+ description: 'A scoped tool with unrelated semantics.',
|
|
|
+ parameters: {},
|
|
|
+ execute() {
|
|
|
+ return Promise.resolve([{ type: 'text', text: 'shadow' }])
|
|
|
+ },
|
|
|
+ }))
|
|
|
+
|
|
|
+ expect(ctx.tools.get('skill', agent)).not.toBe(ctx.tools.get('skill'))
|
|
|
+ expect(await composePrefixForAgent(ctx, agent)).toEqual([])
|
|
|
+ expect(await composePrefix(ctx, '/workspace')).toHaveLength(1)
|
|
|
+ await scope.dispose()
|
|
|
+ })
|
|
|
+
|
|
|
it('validates the catalog description cap', async () => {
|
|
|
const home = await tempDir('tool-invalid-catalog-cap')
|
|
|
const ctx = new Context()
|