@anionex/dsh-tool-search 0.1.1 → 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 +8 -0
- package/README.md +16 -5
- package/README.zh.md +16 -5
- package/lib/client.js +10 -0
- 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 -0
- package/lib/shared.js.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 +12 -0
- package/lib/types/shared.d.ts.map +1 -1
- package/package.json +10 -10
- package/scripts/profile-e2e.mjs +146 -1
- package/src/runtime.ts +123 -16
- package/src/shared.ts +14 -0
- package/tests/presentation.spec.ts +273 -0
- package/tests/runtime.spec.ts +18 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anionex/dsh-tool-search",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Codex-style deferred tool loading for DSH with deterministic local BM25 search",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
]
|
|
84
84
|
},
|
|
85
85
|
"compatibility": {
|
|
86
|
-
"dsh": "^0.1.1-rc.1 || ^0.1.2-alpha.1",
|
|
86
|
+
"dsh": "^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
87
87
|
"profiles": [
|
|
88
88
|
"web",
|
|
89
89
|
"headless"
|
|
@@ -92,14 +92,14 @@
|
|
|
92
92
|
},
|
|
93
93
|
"peerDependencies": {
|
|
94
94
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
95
|
-
"@deepseek-ai/dsh-agent": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1",
|
|
96
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1",
|
|
97
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1",
|
|
98
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1",
|
|
99
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1",
|
|
100
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1",
|
|
101
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1",
|
|
102
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1",
|
|
95
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
96
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
97
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
98
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
99
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
100
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
101
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
102
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
103
103
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
104
104
|
"react": "^18.2.0"
|
|
105
105
|
},
|
package/scripts/profile-e2e.mjs
CHANGED
|
@@ -10,6 +10,17 @@ const packageVersion = JSON.parse(await readFile(join(root, 'package.json'), 'ut
|
|
|
10
10
|
const dsh = process.env.DSH_BIN ?? 'dsh'
|
|
11
11
|
const keepTemp = process.argv.includes('--keep-temp')
|
|
12
12
|
const timeoutMs = Number(process.env.DSH_TOOL_SEARCH_E2E_TIMEOUT_MS ?? 180_000)
|
|
13
|
+
const maxModelRequests = Number(process.env.DSH_TOOL_SEARCH_E2E_MAX_REQUESTS ?? 12)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* PTC presentation is selected by name: DSH renders it as `code` up to 0.1.1
|
|
17
|
+
* and as `ptc` from 0.1.2. Override with DSH_TOOL_SEARCH_PRESENTATION_MODE.
|
|
18
|
+
*/
|
|
19
|
+
function presentationModeFor(version) {
|
|
20
|
+
const override = process.env.DSH_TOOL_SEARCH_PRESENTATION_MODE
|
|
21
|
+
if (override !== undefined && override !== '') return override
|
|
22
|
+
return version.startsWith('0.1.0') || version.startsWith('0.1.1') ? 'code' : 'ptc'
|
|
23
|
+
}
|
|
13
24
|
|
|
14
25
|
function run(command, args, options = {}) {
|
|
15
26
|
return new Promise((resolve, reject) => {
|
|
@@ -129,7 +140,19 @@ async function startLlm(script = [
|
|
|
129
140
|
return
|
|
130
141
|
}
|
|
131
142
|
requests.push(body)
|
|
132
|
-
const
|
|
143
|
+
const names = requestToolNames(body)
|
|
144
|
+
const last = body?.messages?.at(-1)
|
|
145
|
+
process.stderr.write(
|
|
146
|
+
`[e2e] model request #${requests.length} tools=[${names.join(',')}] `
|
|
147
|
+
+ `last=${JSON.stringify(last?.role ?? 'none')}:${JSON.stringify(String(last?.content ?? '').slice(0, 80))}\n`,
|
|
148
|
+
)
|
|
149
|
+
if (requests.length > maxModelRequests) {
|
|
150
|
+
// A runaway loop must fail here instead of growing the recorded bodies.
|
|
151
|
+
response.writeHead(500, { 'content-type': 'application/json' })
|
|
152
|
+
response.end(JSON.stringify({ error: { message: `request-limit-${maxModelRequests}` } }))
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
const step = typeof script === 'function' ? script(body) : script[index++]
|
|
133
156
|
if (step === undefined) {
|
|
134
157
|
response.writeHead(500, { 'content-type': 'application/json' })
|
|
135
158
|
response.end('{"error":{"message":"script-exhausted"}}')
|
|
@@ -191,6 +214,19 @@ function requestToolNames(body) {
|
|
|
191
214
|
return body?.tools?.map(tool => tool?.function?.name).filter(name => typeof name === 'string') ?? []
|
|
192
215
|
}
|
|
193
216
|
|
|
217
|
+
function messageText(body) {
|
|
218
|
+
return body?.messages
|
|
219
|
+
?.filter(message => message?.role === 'system' || message?.role === 'developer')
|
|
220
|
+
.map(message => typeof message.content === 'string' ? message.content : JSON.stringify(message.content ?? ''))
|
|
221
|
+
.join('\n') ?? ''
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function conversationText(body) {
|
|
225
|
+
return body?.messages
|
|
226
|
+
?.map(message => typeof message.content === 'string' ? message.content : JSON.stringify(message.content ?? ''))
|
|
227
|
+
.join('\n') ?? ''
|
|
228
|
+
}
|
|
229
|
+
|
|
194
230
|
function toolResultTexts(body) {
|
|
195
231
|
return body?.messages
|
|
196
232
|
?.filter(message => message?.role === 'tool')
|
|
@@ -361,6 +397,112 @@ try {
|
|
|
361
397
|
const returnFileResult = toolResultTexts(llm.requests[2]).at(-1) ?? ''
|
|
362
398
|
assert(returnFileResult.includes('"sent":true'), 'dsh_im_return_file did not execute through the real Agent loop')
|
|
363
399
|
|
|
400
|
+
const presentationChooser = step => {
|
|
401
|
+
// Under PTC the model only reaches tools through run_code, so the scripted
|
|
402
|
+
// calls are programs that search first and dispatch the selection after.
|
|
403
|
+
const names = requestToolNames(step)
|
|
404
|
+
if (!names.includes('run_code')) return { kind: 'text', text: 'auxiliary call' }
|
|
405
|
+
const transcript = conversationText(step)
|
|
406
|
+
// Match compact and pretty-printed results alike; a literal match loops.
|
|
407
|
+
if (/"sent"\s*:\s*true/u.test(transcript)) return { kind: 'text', text: 'presentation e2e done' }
|
|
408
|
+
if (transcript.includes('dsh_im_return_file') && transcript.includes('tool_search')) {
|
|
409
|
+
return {
|
|
410
|
+
kind: 'tool',
|
|
411
|
+
name: 'run_code',
|
|
412
|
+
arguments: JSON.stringify({
|
|
413
|
+
code: "const sent = await tools.dsh_im_return_file({ path: '/tmp/tool-search-e2e.txt' })\nreturn sent",
|
|
414
|
+
description: 'Return the file through the selected tool',
|
|
415
|
+
}),
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
kind: 'tool',
|
|
420
|
+
name: 'run_code',
|
|
421
|
+
arguments: JSON.stringify({
|
|
422
|
+
code: "const selected = await tools.tool_search({ query: 'dsh_im_return_file', limit: 1 })\nreturn selected",
|
|
423
|
+
description: 'Search deferred tools',
|
|
424
|
+
}),
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Run the presentation pass in one mode, asserting the loader-level guarantees. */
|
|
429
|
+
const runPresentationPass = async mode => {
|
|
430
|
+
const presentationLlm = await startLlm(presentationChooser)
|
|
431
|
+
try {
|
|
432
|
+
const presentationResult = await runDsh([
|
|
433
|
+
'--profile', 'headless',
|
|
434
|
+
'--patch', patch,
|
|
435
|
+
'Find and call the tool that returns a file through the conversation.',
|
|
436
|
+
], {
|
|
437
|
+
cwd: workspaceDirectory,
|
|
438
|
+
env: {
|
|
439
|
+
DSH_HOME: home,
|
|
440
|
+
DSH_TOOLS_MODE: mode,
|
|
441
|
+
DSH_TELEMETRY_DISABLED: '1',
|
|
442
|
+
DSH_PERMISSION_MODE: 'danger-full-access',
|
|
443
|
+
DEEPSEEK_API_KEY: 'tool-search-e2e-key',
|
|
444
|
+
DEEPSEEK_BASE_URL: presentationLlm.baseUrl,
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
assert(
|
|
448
|
+
presentationResult.stdout.trim() === 'presentation e2e done',
|
|
449
|
+
`unexpected ${mode}-mode Agent output: ${presentationResult.stdout}`,
|
|
450
|
+
)
|
|
451
|
+
const agentRequests = presentationLlm.requests.filter(body => requestToolNames(body).includes('run_code'))
|
|
452
|
+
assert(agentRequests.length === 3, `expected 3 ${mode}-mode model calls, received ${agentRequests.length}`)
|
|
453
|
+
const first = agentRequests[0]
|
|
454
|
+
assert(
|
|
455
|
+
requestToolNames(first).join(',') === 'run_code',
|
|
456
|
+
`${mode} assembly exposes more than the reserved transport: ${requestToolNames(first).join(',')}`,
|
|
457
|
+
)
|
|
458
|
+
const firstText = messageText(first)
|
|
459
|
+
assert(firstText.includes('tool_search'), `${mode} SDK omits the searchable tool_search`)
|
|
460
|
+
assert(firstText.includes('bash'), `${mode} SDK omits the always-visible bash`)
|
|
461
|
+
assert(!firstText.includes('fixture_echo'), `${mode} SDK advertises deferred fixture_echo`)
|
|
462
|
+
assert(!firstText.includes('dsh_im_return_file'), `${mode} SDK advertises deferred dsh_im_return_file`)
|
|
463
|
+
const afterSelection = agentRequests[1]
|
|
464
|
+
const afterText = messageText(afterSelection)
|
|
465
|
+
assert(afterText.includes('dsh_im_return_file'), `${mode} SDK omits the tool selected by tool_search`)
|
|
466
|
+
assert(!afterText.includes('fixture_echo'), `${mode} SDK advertises unrelated fixture_echo after search`)
|
|
467
|
+
assert(
|
|
468
|
+
presentationLlm.requests.some(body => /"sent"\s*:\s*true/u.test(conversationText(body))),
|
|
469
|
+
`the selected tool did not dispatch through run_code under ${mode}`,
|
|
470
|
+
)
|
|
471
|
+
return {
|
|
472
|
+
mode,
|
|
473
|
+
modelCalls: agentRequests.length,
|
|
474
|
+
transportOnly: true,
|
|
475
|
+
sdkHasToolSearch: true,
|
|
476
|
+
sdkHidesDeferredBeforeSearch: true,
|
|
477
|
+
sdkAddsSelectionAfterSearch: true,
|
|
478
|
+
selectedToolDispatched: true,
|
|
479
|
+
}
|
|
480
|
+
} finally {
|
|
481
|
+
await presentationLlm.close()
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// The presentation name for the PTC collapse is `code` up to DSH 0.1.1 and
|
|
486
|
+
// `ptc` from 0.1.2. The guess is validated against the host, and a rejected
|
|
487
|
+
// name is retried once with the other spelling.
|
|
488
|
+
const presentationModes = [presentationModeFor(dshVersion)]
|
|
489
|
+
presentationModes.push(presentationModes[0] === 'ptc' ? 'code' : 'ptc')
|
|
490
|
+
let presentation
|
|
491
|
+
let modeRejection
|
|
492
|
+
for (const [index, mode] of presentationModes.entries()) {
|
|
493
|
+
try {
|
|
494
|
+
presentation = await runPresentationPass(mode)
|
|
495
|
+
break
|
|
496
|
+
} catch (error) {
|
|
497
|
+
const text = String(error)
|
|
498
|
+
// The loader reports `expected "native" | "ptc" | "both" but got "code"`.
|
|
499
|
+
const rejectedName = /expected[\s\S]{0,80}but got\s+"[a-z]+"/u.test(text)
|
|
500
|
+
if (!rejectedName || index === presentationModes.length - 1) throw error
|
|
501
|
+
modeRejection = text.split('\n').find(line => line.includes('but got'))?.trim()
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
|
|
364
506
|
process.stdout.write(`${JSON.stringify({
|
|
365
507
|
ok: true,
|
|
366
508
|
dsh: dshVersion,
|
|
@@ -370,6 +512,9 @@ try {
|
|
|
370
512
|
initialToolCount: initialNames.length,
|
|
371
513
|
selectedToolCount: searchedNames.length,
|
|
372
514
|
selectedOnNextCall: searchedNames.includes('dsh_im_return_file'),
|
|
515
|
+
presentation,
|
|
516
|
+
modeRejection,
|
|
517
|
+
dshScript,
|
|
373
518
|
package: `@anionex/dsh-tool-search@${packageVersion}`,
|
|
374
519
|
temporaryHome: keepTemp ? home : undefined,
|
|
375
520
|
}, null, 2)}\n`)
|
package/src/runtime.ts
CHANGED
|
@@ -5,6 +5,9 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
|
5
5
|
import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt'
|
|
6
6
|
import {
|
|
7
7
|
defineTool,
|
|
8
|
+
renderToolsSdk,
|
|
9
|
+
renderToolsSdkPy,
|
|
10
|
+
RUN_CODE_NAME,
|
|
8
11
|
type ToolDefinition,
|
|
9
12
|
type ToolExecution,
|
|
10
13
|
type ToolExecutionResult,
|
|
@@ -16,6 +19,8 @@ import { ToolPolicyResolver, normalizeSettings } from './settings.ts'
|
|
|
16
19
|
import {
|
|
17
20
|
MAX_RESULT_LIMIT,
|
|
18
21
|
TOOL_SEARCH_NAME,
|
|
22
|
+
TOOL_SDK_LANGUAGE_MARKERS,
|
|
23
|
+
TOOL_SDK_SECTION_NAME,
|
|
19
24
|
type CatalogSnapshot,
|
|
20
25
|
type ToolSearchSettings,
|
|
21
26
|
} from './shared.ts'
|
|
@@ -24,6 +29,16 @@ const RESULT_SCHEMA_VERSION = 1
|
|
|
24
29
|
const MAX_QUERY_LENGTH = 4096
|
|
25
30
|
const MAX_DESCRIPTION_LENGTH = 240
|
|
26
31
|
|
|
32
|
+
/** One entry of the generated PTC SDK contract, as `renderToolsSdk` expects it. */
|
|
33
|
+
type SdkSchema = Parameters<typeof renderToolsSdk>[0][number]
|
|
34
|
+
|
|
35
|
+
/** The subset of a registered tool definition the SDK projection consumes. */
|
|
36
|
+
interface ToolDefinitionLike {
|
|
37
|
+
description?: string
|
|
38
|
+
parameters?: unknown
|
|
39
|
+
output?: { schema?: unknown }
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
export interface ToolSearchMatch {
|
|
28
43
|
name: string
|
|
29
44
|
description: string
|
|
@@ -296,31 +311,123 @@ export class ToolSearchRuntime {
|
|
|
296
311
|
return state
|
|
297
312
|
}
|
|
298
313
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
314
|
+
/**
|
|
315
|
+
* The searchable registry view for one Agent: every tool it could reach,
|
|
316
|
+
* whether or not the current presentation shows it.
|
|
317
|
+
*
|
|
318
|
+
* The reserved PTC transport is excluded: it is never searchable, never
|
|
319
|
+
* constrained by policy, and never part of the generated SDK it carries.
|
|
320
|
+
* Reading the registry (rather than the assembled schemas) is what keeps the
|
|
321
|
+
* corpus complete under PTC presentation, where the assembly itself carries
|
|
322
|
+
* only `run_code`.
|
|
323
|
+
*/
|
|
324
|
+
private refreshCatalog(state: AgentState): ToolCatalogEntry[] {
|
|
325
|
+
if (state.catalogGeneration !== this.generation) {
|
|
326
|
+
state.catalog = buildCatalog(
|
|
327
|
+
(this.ctx.tools.schemas(state.agent) as ToolSchemaLike[])
|
|
328
|
+
.filter(tool => tool.name !== RUN_CODE_NAME),
|
|
329
|
+
)
|
|
330
|
+
state.catalogGeneration = this.generation
|
|
331
|
+
}
|
|
332
|
+
return state.catalog
|
|
303
333
|
}
|
|
304
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Hide every deferred tool from one final assembly.
|
|
337
|
+
*
|
|
338
|
+
* Two surfaces carry tools and both must agree: the native schema list plus
|
|
339
|
+
* its `tool:<name>` guidance sections, and — under `ptc`/`both` — the
|
|
340
|
+
* generated `tools:sdk` section. The SDK text is regenerated with the same
|
|
341
|
+
* renderer `dsh-tools` uses, from the surviving tools alone, so the filtered
|
|
342
|
+
* assembly is byte-identical to the one the registry would have built for
|
|
343
|
+
* that smaller tool set.
|
|
344
|
+
*/
|
|
305
345
|
private filterAssembly(state: AgentState, assembly: PromptAssembly): PromptAssembly {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
state.catalog = buildCatalog(assembly.tools as ToolSchemaLike[])
|
|
310
|
-
state.catalogGeneration = this.generation
|
|
346
|
+
const catalog = this.refreshCatalog(state)
|
|
347
|
+
const known = new Set(catalog.map(tool => tool.name))
|
|
311
348
|
const visible = new Set(
|
|
312
|
-
|
|
349
|
+
catalog
|
|
313
350
|
.map(tool => tool.name)
|
|
314
351
|
.filter(name => this.policy.classify(name, state.selected) === 'always'),
|
|
315
352
|
)
|
|
316
|
-
const hidden = new Set(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
353
|
+
const hidden = new Set(catalog.map(tool => tool.name).filter(name => !visible.has(name)))
|
|
354
|
+
const tools = assembly.tools.filter(tool => !known.has(tool.name) || visible.has(tool.name))
|
|
355
|
+
let sdkRewritten = false
|
|
356
|
+
const sections = assembly.sections
|
|
357
|
+
.filter(section => {
|
|
321
358
|
const name = toolSectionName(section.name)
|
|
322
359
|
return name === undefined || !hidden.has(name)
|
|
323
|
-
})
|
|
360
|
+
})
|
|
361
|
+
.map(section => {
|
|
362
|
+
// An empty SDK section is an Agent-scoped opt-out of PTC presentation
|
|
363
|
+
// (a native override under a PTC deployment): there is nothing to filter.
|
|
364
|
+
if (section.name !== TOOL_SDK_SECTION_NAME || hidden.size === 0 || section.text.trim().length === 0) {
|
|
365
|
+
return section
|
|
366
|
+
}
|
|
367
|
+
const text = this.renderSdk(state, visible, section.text)
|
|
368
|
+
if (text === section.text) return section
|
|
369
|
+
sdkRewritten = true
|
|
370
|
+
return { ...section, text }
|
|
371
|
+
})
|
|
372
|
+
if (tools.length === assembly.tools.length && sections.length === assembly.sections.length && !sdkRewritten) {
|
|
373
|
+
return assembly
|
|
374
|
+
}
|
|
375
|
+
return { ...assembly, tools, sections }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Rebuild the PTC SDK section for the surviving tools.
|
|
380
|
+
*
|
|
381
|
+
* The flavor must be the one the session already programs against, so it
|
|
382
|
+
* comes from the mounted code runtime; a host that cannot answer for it
|
|
383
|
+
* falls back to the marker the existing section was rendered with.
|
|
384
|
+
*/
|
|
385
|
+
private renderSdk(state: AgentState, visible: ReadonlySet<string>, original: string): string {
|
|
386
|
+
const render = this.resolveSdkRenderer(original)
|
|
387
|
+
const schemas: SdkSchema[] = []
|
|
388
|
+
for (const tool of state.catalog) {
|
|
389
|
+
if (!visible.has(tool.name)) continue
|
|
390
|
+
const definition = this.toolDefinition(tool.name, state.agent)
|
|
391
|
+
schemas.push({
|
|
392
|
+
name: tool.name,
|
|
393
|
+
description: definition?.description ?? tool.description,
|
|
394
|
+
parameters: cloneJson(definition?.parameters ?? tool.parameters) as SdkSchema['parameters'],
|
|
395
|
+
output: cloneJson(definition?.output?.schema ?? {}) as SdkSchema['output'],
|
|
396
|
+
})
|
|
324
397
|
}
|
|
398
|
+
return render(schemas)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private resolveSdkRenderer(original: string): (schemas: SdkSchema[]) => string {
|
|
402
|
+
const runtime = this.codeRuntime()
|
|
403
|
+
if (runtime.language === 'typescript') return renderToolsSdk
|
|
404
|
+
if (runtime.language === 'python') return renderToolsSdkPy
|
|
405
|
+
if (original.includes(TOOL_SDK_LANGUAGE_MARKERS.python)) return renderToolsSdkPy
|
|
406
|
+
if (original.includes(TOOL_SDK_LANGUAGE_MARKERS.typescript)) return renderToolsSdk
|
|
407
|
+
throw new Error(
|
|
408
|
+
`${TOOL_SEARCH_NAME}: cannot filter the generated tool SDK for code runtime language `
|
|
409
|
+
+ `${JSON.stringify(runtime.language ?? 'unknown')}`,
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private codeRuntime(): { language?: unknown } {
|
|
414
|
+
const runtime = (this.ctx as unknown as { get?: (name: string) => unknown }).get?.('codeRuntime')
|
|
415
|
+
return typeof runtime === 'object' && runtime !== null ? runtime as { language?: unknown } : {}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private toolDefinition(name: string, agent: Agent): ToolDefinitionLike | undefined {
|
|
419
|
+
const registry = this.ctx.tools as unknown as { get?: (name: string, scope?: unknown) => unknown }
|
|
420
|
+
if (typeof registry.get !== 'function') return undefined
|
|
421
|
+
const definition = registry.get(name, agent)
|
|
422
|
+
return typeof definition === 'object' && definition !== null ? definition as ToolDefinitionLike : undefined
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Lossless clone of a schema node; a value that cannot be cloned is used as-is. */
|
|
427
|
+
function cloneJson<T>(value: T): T {
|
|
428
|
+
try {
|
|
429
|
+
return structuredClone(value)
|
|
430
|
+
} catch {
|
|
431
|
+
return value
|
|
325
432
|
}
|
|
326
433
|
}
|
package/src/shared.ts
CHANGED
|
@@ -6,6 +6,20 @@ export const TOOL_SEARCH_CATALOG_ROUTE = '/_dsh/tool-search/catalog'
|
|
|
6
6
|
export const DEFAULT_RESULT_LIMIT = 5
|
|
7
7
|
export const MAX_RESULT_LIMIT = 20
|
|
8
8
|
|
|
9
|
+
/** Prompt section carrying the generated PTC tool SDK (`ptc` and `both` modes). */
|
|
10
|
+
export const TOOL_SDK_SECTION_NAME = 'tools:sdk'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Markers identifying which renderer produced an SDK section we did not
|
|
14
|
+
* assemble ourselves. Used only when the code runtime cannot be read from the
|
|
15
|
+
* context, because the renderer must match the flavor the model was told to
|
|
16
|
+
* program against.
|
|
17
|
+
*/
|
|
18
|
+
export const TOOL_SDK_LANGUAGE_MARKERS = Object.freeze({
|
|
19
|
+
typescript: 'interface ToolArgsMap',
|
|
20
|
+
python: 'class Tools(Protocol)',
|
|
21
|
+
} as const)
|
|
22
|
+
|
|
9
23
|
/**
|
|
10
24
|
* Stable bootstrap surface for current DSH releases. Unknown future first-party
|
|
11
25
|
* names are deferred until this list or the user allowlist is updated because
|