@anionex/dsh-tool-search 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +20 -10
- package/README.zh.md +20 -10
- package/docs/benchmark-results.json +1 -1
- package/lib/client.js +13 -2
- package/lib/client.js.map +1 -1
- package/lib/runtime.js +111 -21
- package/lib/runtime.js.map +1 -1
- package/lib/shared.js +12 -1
- package/lib/shared.js.map +1 -1
- package/lib/types/client/index.d.ts +1 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/runtime.d.ts +31 -0
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/shared.d.ts +13 -1
- package/lib/types/shared.d.ts.map +1 -1
- package/package.json +10 -10
- package/scripts/profile-e2e.mjs +169 -11
- package/scripts/validate.mjs +1 -1
- package/src/client/index.tsx +5 -1
- package/src/runtime.ts +123 -16
- package/src/shared.ts +14 -1
- package/tests/client.spec.tsx +10 -1
- package/tests/presentation.spec.ts +273 -0
- package/tests/runtime.spec.ts +18 -3
- package/tests/settings.spec.ts +1 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
2
|
+
import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt'
|
|
3
|
+
import { renderToolsSdk, renderToolsSdkPy, RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { ToolSearchRuntime, type ToolSearchResult } from '../src/runtime.ts'
|
|
6
|
+
import { TOOL_SEARCH_NAME, type ToolSearchSettings } from '../src/shared.ts'
|
|
7
|
+
|
|
8
|
+
type AssemblyListener = (
|
|
9
|
+
assembly: PromptAssembly,
|
|
10
|
+
context: object,
|
|
11
|
+
next: () => Promise<PromptAssembly>,
|
|
12
|
+
) => Promise<PromptAssembly>
|
|
13
|
+
|
|
14
|
+
type RootListener = (...args: unknown[]) => unknown
|
|
15
|
+
|
|
16
|
+
interface FixtureTool {
|
|
17
|
+
name: string
|
|
18
|
+
description: string
|
|
19
|
+
parameters: Record<string, unknown>
|
|
20
|
+
output: Record<string, unknown>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fixtureTool(name: string, description: string): FixtureTool {
|
|
24
|
+
return {
|
|
25
|
+
name,
|
|
26
|
+
description,
|
|
27
|
+
parameters: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
properties: {
|
|
31
|
+
target: { type: 'string', description: `${description} target.` },
|
|
32
|
+
},
|
|
33
|
+
required: ['target'],
|
|
34
|
+
},
|
|
35
|
+
output: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
properties: { ok: { type: 'boolean', required: true } },
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEFAULT_TOOLS: FixtureTool[] = [
|
|
44
|
+
fixtureTool('apply_patch', 'Edit files with a patch'),
|
|
45
|
+
fixtureTool(TOOL_SEARCH_NAME, 'Search deferred tools'),
|
|
46
|
+
fixtureTool('browser_take_screenshot', 'Capture a browser screenshot'),
|
|
47
|
+
fixtureTool('web_search', 'Search current public web sources'),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
/** The `tools:sdk` body a real registry renders for one tool set. */
|
|
51
|
+
function sdkText(tools: readonly FixtureTool[], render = renderToolsSdk): string {
|
|
52
|
+
return render(tools.map(tool => ({
|
|
53
|
+
name: tool.name,
|
|
54
|
+
description: tool.description,
|
|
55
|
+
parameters: tool.parameters,
|
|
56
|
+
output: tool.output,
|
|
57
|
+
})))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function makeAgent(id: string) {
|
|
61
|
+
let assemblyListener: AssemblyListener | undefined
|
|
62
|
+
const agent = {
|
|
63
|
+
id,
|
|
64
|
+
session: { events: [] },
|
|
65
|
+
ctx: {
|
|
66
|
+
on(name: string, listener: AssemblyListener) {
|
|
67
|
+
if (name === 'system-prompt/assemble') assemblyListener = listener
|
|
68
|
+
return () => {
|
|
69
|
+
if (assemblyListener === listener) assemblyListener = undefined
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
} as unknown as Agent
|
|
74
|
+
return {
|
|
75
|
+
agent,
|
|
76
|
+
assemble(assembly: PromptAssembly): Promise<PromptAssembly> {
|
|
77
|
+
if (assemblyListener === undefined) throw new Error('assembly listener was not installed')
|
|
78
|
+
return assemblyListener(assembly, {}, () => Promise.resolve(assembly))
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Harness for one presentation mode. The registry view carries every tool plus
|
|
85
|
+
* the reserved transport; each mode then builds the assembly the real registry
|
|
86
|
+
* would build: `ptc` collapses the schema list to `run_code` and adds the
|
|
87
|
+
* generated SDK, `both` keeps both surfaces.
|
|
88
|
+
*/
|
|
89
|
+
function harness(options: {
|
|
90
|
+
mode: 'native' | 'ptc' | 'both'
|
|
91
|
+
language?: string
|
|
92
|
+
settings?: Partial<ToolSearchSettings>
|
|
93
|
+
tools?: FixtureTool[]
|
|
94
|
+
withCodeRuntime?: boolean
|
|
95
|
+
} ) {
|
|
96
|
+
const tools = options.tools ?? DEFAULT_TOOLS
|
|
97
|
+
const agent = makeAgent('agent-1')
|
|
98
|
+
const listeners = new Map<string, RootListener[]>()
|
|
99
|
+
const ctx = {
|
|
100
|
+
agents: { list: () => [agent.agent] },
|
|
101
|
+
tools: {
|
|
102
|
+
// The registry view: every tool the Agent could reach. The reserved
|
|
103
|
+
// transport joins it only where PTC presentation is actually selected.
|
|
104
|
+
schemas: () => options.mode === 'native'
|
|
105
|
+
? [...tools]
|
|
106
|
+
: [...tools, { name: RUN_CODE_NAME, description: 'Run a program', parameters: {} }],
|
|
107
|
+
// Registered definitions carry the output contract as `{ schema, render }`.
|
|
108
|
+
get: (name: string) => {
|
|
109
|
+
const tool = tools.find(candidate => candidate.name === name)
|
|
110
|
+
if (tool === undefined) return undefined
|
|
111
|
+
return { ...tool, output: { schema: tool.output, render: () => [] } }
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
get: (name: string) =>
|
|
115
|
+
name === 'codeRuntime' && options.withCodeRuntime !== false
|
|
116
|
+
? { language: options.language ?? 'typescript' }
|
|
117
|
+
: undefined,
|
|
118
|
+
logger: { warn: vi.fn() },
|
|
119
|
+
on(name: string, listener: RootListener) {
|
|
120
|
+
const rows = listeners.get(name) ?? []
|
|
121
|
+
rows.push(listener)
|
|
122
|
+
listeners.set(name, rows)
|
|
123
|
+
return () => listeners.set(name, rows.filter(row => row !== listener))
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
const runtime = new ToolSearchRuntime(ctx as never, {
|
|
127
|
+
defaultLimit: 5,
|
|
128
|
+
alwaysVisible: [],
|
|
129
|
+
neverSearch: [],
|
|
130
|
+
...options.settings,
|
|
131
|
+
})
|
|
132
|
+
const dispose = runtime.install()
|
|
133
|
+
const render = options.language === 'python' ? renderToolsSdkPy : renderToolsSdk
|
|
134
|
+
const assembly = (): PromptAssembly => {
|
|
135
|
+
const sections = tools.map(tool => ({ name: `tool:${tool.name}`, text: `${tool.name} guidance` }))
|
|
136
|
+
if (options.mode !== 'native') {
|
|
137
|
+
sections.unshift({ name: 'tools:ptc-only', text: `\`${RUN_CODE_NAME}\` is the only tool you can call directly.` })
|
|
138
|
+
sections.push({ name: 'tools:sdk', text: sdkText(tools, render) })
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
sections,
|
|
142
|
+
contexts: [],
|
|
143
|
+
tools: (options.mode === 'native'
|
|
144
|
+
? [...tools]
|
|
145
|
+
: options.mode === 'ptc'
|
|
146
|
+
? [{ name: RUN_CODE_NAME, description: 'Run a program', parameters: {} }]
|
|
147
|
+
: [...tools, { name: RUN_CODE_NAME, description: 'Run a program', parameters: {} }]) as never,
|
|
148
|
+
variables: {},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
agent: agent.agent,
|
|
153
|
+
runtime,
|
|
154
|
+
dispose,
|
|
155
|
+
assemble: () => agent.assemble(assembly()),
|
|
156
|
+
assembleRaw: (value: PromptAssembly) => agent.assemble(value),
|
|
157
|
+
raw: assembly,
|
|
158
|
+
async search(query: string, limit?: number): Promise<ToolSearchResult> {
|
|
159
|
+
const exec = {
|
|
160
|
+
agent: agent.agent,
|
|
161
|
+
name: TOOL_SEARCH_NAME,
|
|
162
|
+
callId: 'call-search',
|
|
163
|
+
rootCallId: 'call-search',
|
|
164
|
+
token: Symbol('test-token'),
|
|
165
|
+
arguments: { query },
|
|
166
|
+
signal: new AbortController().signal,
|
|
167
|
+
}
|
|
168
|
+
const value = await runtime.definition.execute(
|
|
169
|
+
{ query, ...(limit === undefined ? {} : { limit }) },
|
|
170
|
+
exec as never,
|
|
171
|
+
) as ToolSearchResult
|
|
172
|
+
for (const listener of listeners.get('tools/result') ?? []) {
|
|
173
|
+
listener(exec, { isError: false, value, content: [{ type: 'text', text: JSON.stringify(value) }] })
|
|
174
|
+
}
|
|
175
|
+
return value
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sdkSection(assembly: PromptAssembly): string {
|
|
181
|
+
const section = assembly.sections.find(candidate => candidate.name === 'tools:sdk')
|
|
182
|
+
if (section === undefined) throw new Error('assembly carries no tools:sdk section')
|
|
183
|
+
return section.text
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
describe('PTC presentation support', () => {
|
|
187
|
+
it('filters the generated SDK and keeps the transport in ptc mode', async () => {
|
|
188
|
+
const app = harness({ mode: 'ptc' })
|
|
189
|
+
const assembly = await app.assemble()
|
|
190
|
+
|
|
191
|
+
expect(assembly.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
|
|
192
|
+
expect(sdkSection(assembly)).toBe(sdkText([
|
|
193
|
+
DEFAULT_TOOLS[0]!,
|
|
194
|
+
DEFAULT_TOOLS[1]!,
|
|
195
|
+
]))
|
|
196
|
+
expect(sdkSection(assembly)).not.toContain('web_search')
|
|
197
|
+
expect(sdkSection(assembly)).not.toContain('browser_take_screenshot')
|
|
198
|
+
expect(sdkSection(assembly)).toContain('apply_patch')
|
|
199
|
+
expect(sdkSection(assembly)).toContain(TOOL_SEARCH_NAME)
|
|
200
|
+
expect(assembly.sections.map(section => section.name)).toContain('tools:ptc-only')
|
|
201
|
+
expect(assembly.sections.map(section => section.name)).not.toContain('tool:web_search')
|
|
202
|
+
app.dispose()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('adds tools to the SDK on the next assembly after a successful search', async () => {
|
|
206
|
+
const app = harness({ mode: 'ptc' })
|
|
207
|
+
const before = await app.assemble()
|
|
208
|
+
expect(sdkSection(before)).not.toContain('web_search')
|
|
209
|
+
|
|
210
|
+
const result = await app.search('web search public sources', 1)
|
|
211
|
+
expect(result.tools.map(tool => tool.name)).toEqual(['web_search'])
|
|
212
|
+
|
|
213
|
+
const after = await app.assemble()
|
|
214
|
+
expect(sdkSection(after)).toContain('web_search')
|
|
215
|
+
expect(sdkSection(after)).toBe(sdkText([
|
|
216
|
+
DEFAULT_TOOLS[0]!,
|
|
217
|
+
DEFAULT_TOOLS[1]!,
|
|
218
|
+
DEFAULT_TOOLS[3]!,
|
|
219
|
+
]))
|
|
220
|
+
expect(after.sections.map(section => section.name)).toContain('tool:web_search')
|
|
221
|
+
app.dispose()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('filters both surfaces under the both presentation', async () => {
|
|
225
|
+
const app = harness({ mode: 'both' })
|
|
226
|
+
const assembly = await app.assemble()
|
|
227
|
+
|
|
228
|
+
expect(assembly.tools.map(tool => tool.name)).toEqual([
|
|
229
|
+
'apply_patch',
|
|
230
|
+
TOOL_SEARCH_NAME,
|
|
231
|
+
RUN_CODE_NAME,
|
|
232
|
+
])
|
|
233
|
+
expect(sdkSection(assembly)).toBe(sdkText([DEFAULT_TOOLS[0]!, DEFAULT_TOOLS[1]!]))
|
|
234
|
+
app.dispose()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('renders the SDK flavor the mounted code runtime selects', async () => {
|
|
238
|
+
const app = harness({ mode: 'ptc', language: 'python' })
|
|
239
|
+
const assembly = await app.assemble()
|
|
240
|
+
|
|
241
|
+
expect(sdkSection(assembly)).toBe(sdkText([DEFAULT_TOOLS[0]!, DEFAULT_TOOLS[1]!], renderToolsSdkPy))
|
|
242
|
+
expect(sdkSection(assembly)).toContain('class Tools(Protocol)')
|
|
243
|
+
app.dispose()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('reads the flavor from the section when the runtime is unreachable', async () => {
|
|
247
|
+
const app = harness({ mode: 'ptc', withCodeRuntime: false })
|
|
248
|
+
const assembly = await app.assemble()
|
|
249
|
+
|
|
250
|
+
expect(sdkSection(assembly)).toBe(sdkText([DEFAULT_TOOLS[0]!, DEFAULT_TOOLS[1]!]))
|
|
251
|
+
app.dispose()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('leaves an Agent-scoped native override (empty SDK section) untouched', async () => {
|
|
255
|
+
const app = harness({ mode: 'native' })
|
|
256
|
+
const raw = app.raw()
|
|
257
|
+
raw.sections.push({ name: 'tools:sdk', text: '' })
|
|
258
|
+
const assembly = await app.assembleRaw(raw)
|
|
259
|
+
expect(assembly.tools.map(tool => tool.name)).toEqual(['apply_patch', TOOL_SEARCH_NAME])
|
|
260
|
+
app.dispose()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('returns the identical assembly when no tool is hidden', async () => {
|
|
264
|
+
const app = harness({
|
|
265
|
+
mode: 'ptc',
|
|
266
|
+
settings: { alwaysVisible: DEFAULT_TOOLS.map(tool => tool.name) },
|
|
267
|
+
})
|
|
268
|
+
const raw = app.raw()
|
|
269
|
+
const assembly = await app.assembleRaw(raw)
|
|
270
|
+
expect(assembly).toBe(raw)
|
|
271
|
+
app.dispose()
|
|
272
|
+
})
|
|
273
|
+
})
|
package/tests/runtime.spec.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
2
2
|
import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt'
|
|
3
|
+
import { renderToolsSdk } from '@deepseek-ai/dsh-tools'
|
|
3
4
|
import { describe, expect, it, vi } from 'vitest'
|
|
4
5
|
import type { ToolSchemaLike } from '../src/catalog.ts'
|
|
5
6
|
import { ToolSearchRuntime, type ToolSearchResult } from '../src/runtime.ts'
|
|
@@ -251,11 +252,25 @@ describe('ToolSearchRuntime', () => {
|
|
|
251
252
|
app.dispose()
|
|
252
253
|
})
|
|
253
254
|
|
|
254
|
-
it('
|
|
255
|
+
it('filters the generated SDK section instead of rejecting PTC and both assemblies', async () => {
|
|
255
256
|
const app = harness()
|
|
256
257
|
const assembly = app.assembly()
|
|
257
|
-
assembly.sections.push({
|
|
258
|
-
|
|
258
|
+
assembly.sections.push({
|
|
259
|
+
name: 'tools:sdk',
|
|
260
|
+
text: renderToolsSdk(assembly.tools.map(tool => ({
|
|
261
|
+
name: tool.name,
|
|
262
|
+
description: tool.description ?? '',
|
|
263
|
+
parameters: tool.parameters,
|
|
264
|
+
output: {},
|
|
265
|
+
}))),
|
|
266
|
+
})
|
|
267
|
+
const filtered = await app.agents[0]!.assemble(assembly)
|
|
268
|
+
expect(filtered.tools.map(tool => tool.name)).toEqual(['apply_patch', TOOL_SEARCH_NAME])
|
|
269
|
+
const sdk = filtered.sections.find(section => section.name === 'tools:sdk')?.text ?? ''
|
|
270
|
+
expect(sdk).toContain('apply_patch')
|
|
271
|
+
expect(sdk).toContain(TOOL_SEARCH_NAME)
|
|
272
|
+
expect(sdk).not.toContain('web_search')
|
|
273
|
+
expect(sdk).not.toContain('browser_take_screenshot')
|
|
259
274
|
app.dispose()
|
|
260
275
|
})
|
|
261
276
|
|
package/tests/settings.spec.ts
CHANGED
|
@@ -24,6 +24,7 @@ describe('tool exposure policy', () => {
|
|
|
24
24
|
expect(policy.classify(TOOL_SEARCH_NAME)).toBe('always')
|
|
25
25
|
expect(policy.classify('apply_patch')).toBe('always')
|
|
26
26
|
expect(policy.classify('report')).toBe('always')
|
|
27
|
+
expect(policy.classify('dsh_im_return_file')).toBe('deferred')
|
|
27
28
|
expect(policy.classify('bash')).toBe('blocked')
|
|
28
29
|
expect(policy.classify('custom_tool')).toBe('always')
|
|
29
30
|
expect(policy.classify('conflict_tool')).toBe('blocked')
|