@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
package/lib/types/runtime.d.ts
CHANGED
|
@@ -31,7 +31,38 @@ export declare class ToolSearchRuntime {
|
|
|
31
31
|
private attach;
|
|
32
32
|
private detach;
|
|
33
33
|
private requireState;
|
|
34
|
+
/**
|
|
35
|
+
* The searchable registry view for one Agent: every tool it could reach,
|
|
36
|
+
* whether or not the current presentation shows it.
|
|
37
|
+
*
|
|
38
|
+
* The reserved PTC transport is excluded: it is never searchable, never
|
|
39
|
+
* constrained by policy, and never part of the generated SDK it carries.
|
|
40
|
+
* Reading the registry (rather than the assembled schemas) is what keeps the
|
|
41
|
+
* corpus complete under PTC presentation, where the assembly itself carries
|
|
42
|
+
* only `run_code`.
|
|
43
|
+
*/
|
|
34
44
|
private refreshCatalog;
|
|
45
|
+
/**
|
|
46
|
+
* Hide every deferred tool from one final assembly.
|
|
47
|
+
*
|
|
48
|
+
* Two surfaces carry tools and both must agree: the native schema list plus
|
|
49
|
+
* its `tool:<name>` guidance sections, and — under `ptc`/`both` — the
|
|
50
|
+
* generated `tools:sdk` section. The SDK text is regenerated with the same
|
|
51
|
+
* renderer `dsh-tools` uses, from the surviving tools alone, so the filtered
|
|
52
|
+
* assembly is byte-identical to the one the registry would have built for
|
|
53
|
+
* that smaller tool set.
|
|
54
|
+
*/
|
|
35
55
|
private filterAssembly;
|
|
56
|
+
/**
|
|
57
|
+
* Rebuild the PTC SDK section for the surviving tools.
|
|
58
|
+
*
|
|
59
|
+
* The flavor must be the one the session already programs against, so it
|
|
60
|
+
* comes from the mounted code runtime; a host that cannot answer for it
|
|
61
|
+
* falls back to the marker the existing section was rendered with.
|
|
62
|
+
*/
|
|
63
|
+
private renderSdk;
|
|
64
|
+
private resolveSdkRenderer;
|
|
65
|
+
private codeRuntime;
|
|
66
|
+
private toolDefinition;
|
|
36
67
|
}
|
|
37
68
|
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAElE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AAEnD,OAAO,
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAElE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AAEnD,OAAO,EAKL,KAAK,cAAc,EAIpB,MAAM,wBAAwB,CAAA;AAI/B,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACxB,MAAM,aAAa,CAAA;AAgBpB,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,CAAC,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,eAAe,EAAE,CAAA;CACzB;AAqFD,qBAAa,iBAAiB;IAQ1B,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAA;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAoB;IACpC,OAAO,CAAC,UAAU,CAAI;gBAGH,GAAG,EAAE,OAAO,EAC7B,QAAQ,EAAE,kBAAkB;IAO9B,OAAO,IAAI,MAAM,IAAI;IAgBrB,cAAc,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI;IAK/C,QAAQ,IAAI,eAAe;IAsB3B,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC;IAI9C,OAAO,CAAC,gBAAgB;IAsDxB,OAAO,CAAC,MAAM;IAuBd,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,MAAM;IAkBd,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,YAAY;IAOpB;;;;;;;;;OASG;IACH,OAAO,CAAC,cAAc;IAWtB;;;;;;;;;OASG;IACH,OAAO,CAAC,cAAc;IAiCtB;;;;;;OAMG;IACH,OAAO,CAAC,SAAS;IAgBjB,OAAO,CAAC,kBAAkB;IAY1B,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,cAAc;CAMvB"}
|
package/lib/types/shared.d.ts
CHANGED
|
@@ -4,12 +4,24 @@ export declare const TOOL_SEARCH_SETTINGS_NAMESPACE = "tool-search";
|
|
|
4
4
|
export declare const TOOL_SEARCH_CATALOG_ROUTE = "/_dsh/tool-search/catalog";
|
|
5
5
|
export declare const DEFAULT_RESULT_LIMIT = 5;
|
|
6
6
|
export declare const MAX_RESULT_LIMIT = 20;
|
|
7
|
+
/** Prompt section carrying the generated PTC tool SDK (`ptc` and `both` modes). */
|
|
8
|
+
export declare const TOOL_SDK_SECTION_NAME = "tools:sdk";
|
|
9
|
+
/**
|
|
10
|
+
* Markers identifying which renderer produced an SDK section we did not
|
|
11
|
+
* assemble ourselves. Used only when the code runtime cannot be read from the
|
|
12
|
+
* context, because the renderer must match the flavor the model was told to
|
|
13
|
+
* program against.
|
|
14
|
+
*/
|
|
15
|
+
export declare const TOOL_SDK_LANGUAGE_MARKERS: Readonly<{
|
|
16
|
+
readonly typescript: "interface ToolArgsMap";
|
|
17
|
+
readonly python: "class Tools(Protocol)";
|
|
18
|
+
}>;
|
|
7
19
|
/**
|
|
8
20
|
* Stable bootstrap surface for current DSH releases. Unknown future first-party
|
|
9
21
|
* names are deferred until this list or the user allowlist is updated because
|
|
10
22
|
* ToolDefinition does not expose package provenance.
|
|
11
23
|
*/
|
|
12
|
-
export declare const DEFAULT_CORE_TOOLS: readonly ["apply_patch", "ask_user_question", "bash", "create_goal", "
|
|
24
|
+
export declare const DEFAULT_CORE_TOOLS: readonly ["apply_patch", "ask_user_question", "bash", "create_goal", "exit_plan_mode", "get_goal", "interrupt_agent", "job_kill", "job_list", "job_output", "list_agents", "ralph", "report", "send_message", "skill", "subagent", "subagent_fork", "todo_write", "update_goal", "workflow"];
|
|
13
25
|
export interface ToolSearchSettings {
|
|
14
26
|
defaultLimit: number;
|
|
15
27
|
alwaysVisible: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/shared.ts"],"names":[],"mappings":"AAAA,iEAAiE;AAEjE,eAAO,MAAM,gBAAgB,gBAAgB,CAAA;AAC7C,eAAO,MAAM,8BAA8B,gBAAgB,CAAA;AAC3D,eAAO,MAAM,yBAAyB,8BAA8B,CAAA;AACpE,eAAO,MAAM,oBAAoB,IAAI,CAAA;AACrC,eAAO,MAAM,gBAAgB,KAAK,CAAA;AAElC;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/shared.ts"],"names":[],"mappings":"AAAA,iEAAiE;AAEjE,eAAO,MAAM,gBAAgB,gBAAgB,CAAA;AAC7C,eAAO,MAAM,8BAA8B,gBAAgB,CAAA;AAC3D,eAAO,MAAM,yBAAyB,8BAA8B,CAAA;AACpE,eAAO,MAAM,oBAAoB,IAAI,CAAA;AACrC,eAAO,MAAM,gBAAgB,KAAK,CAAA;AAElC,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,cAAc,CAAA;AAEhD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB;;;EAG3B,CAAA;AAEX;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,8RAqBpB,CAAA;AAEX,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB;AAED,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,kBAAkB,CAIxD,CAAA;AAEF,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAA;AAE1D,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,UAAU,CAAA;IAClB,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,CAAC,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,kBAAkB,EAAE,CAAA;CAC5B"}
|
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
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
-
import { access, mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { access, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { createServer } from 'node:http'
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { dirname, join } from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
|
|
8
8
|
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
9
|
+
const packageVersion = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')).version
|
|
9
10
|
const dsh = process.env.DSH_BIN ?? 'dsh'
|
|
10
11
|
const keepTemp = process.argv.includes('--keep-temp')
|
|
11
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
|
+
}
|
|
12
24
|
|
|
13
25
|
function run(command, args, options = {}) {
|
|
14
26
|
return new Promise((resolve, reject) => {
|
|
@@ -88,14 +100,24 @@ async function fixturePackage(directory) {
|
|
|
88
100
|
' },',
|
|
89
101
|
' execute: args => Promise.resolve({ echo: args.value }),',
|
|
90
102
|
' }))',
|
|
103
|
+
' ctx.tools.register(defineTool({',
|
|
104
|
+
" name: 'dsh_im_return_file',",
|
|
105
|
+
" description: 'Send a readable file to the user through the conversation.',",
|
|
106
|
+
" parameters: { path: { type: 'string', required: true, description: 'Readable file path.' } },",
|
|
107
|
+
' output: {',
|
|
108
|
+
" schema: { type: 'object', additionalProperties: false, properties: { sent: { type: 'boolean', required: true } } },",
|
|
109
|
+
" render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],",
|
|
110
|
+
' },',
|
|
111
|
+
' execute: () => Promise.resolve({ sent: true }),',
|
|
112
|
+
' }))',
|
|
91
113
|
'}',
|
|
92
114
|
'',
|
|
93
115
|
].join('\n'))
|
|
94
116
|
}
|
|
95
117
|
|
|
96
118
|
async function startLlm(script = [
|
|
97
|
-
{ kind: 'tool', name: 'tool_search', arguments: '{"query":"
|
|
98
|
-
{ kind: 'tool', name: '
|
|
119
|
+
{ kind: 'tool', name: 'tool_search', arguments: '{"query":"dsh_im_return_file","limit":1}' },
|
|
120
|
+
{ kind: 'tool', name: 'dsh_im_return_file', arguments: '{"path":"/tmp/tool-search-e2e.txt"}' },
|
|
99
121
|
{ kind: 'text', text: 'tool search e2e done' },
|
|
100
122
|
]) {
|
|
101
123
|
const requests = []
|
|
@@ -118,7 +140,19 @@ async function startLlm(script = [
|
|
|
118
140
|
return
|
|
119
141
|
}
|
|
120
142
|
requests.push(body)
|
|
121
|
-
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++]
|
|
122
156
|
if (step === undefined) {
|
|
123
157
|
response.writeHead(500, { 'content-type': 'application/json' })
|
|
124
158
|
response.end('{"error":{"message":"script-exhausted"}}')
|
|
@@ -180,6 +214,19 @@ function requestToolNames(body) {
|
|
|
180
214
|
return body?.tools?.map(tool => tool?.function?.name).filter(name => typeof name === 'string') ?? []
|
|
181
215
|
}
|
|
182
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
|
+
|
|
183
230
|
function toolResultTexts(body) {
|
|
184
231
|
return body?.messages
|
|
185
232
|
?.filter(message => message?.role === 'tool')
|
|
@@ -323,7 +370,7 @@ try {
|
|
|
323
370
|
const runResult = await runDsh([
|
|
324
371
|
'--profile', 'headless',
|
|
325
372
|
'--patch', patch,
|
|
326
|
-
'Find and call the
|
|
373
|
+
'Find and call the tool that returns a file through the conversation.',
|
|
327
374
|
], {
|
|
328
375
|
cwd: workspaceDirectory,
|
|
329
376
|
env: {
|
|
@@ -341,12 +388,120 @@ try {
|
|
|
341
388
|
const searchedNames = requestToolNames(llm.requests[1])
|
|
342
389
|
assert(initialNames.includes('tool_search'), 'initial model call omits tool_search')
|
|
343
390
|
assert(!initialNames.includes('fixture_echo'), 'initial model call exposes deferred fixture_echo')
|
|
344
|
-
assert(
|
|
391
|
+
assert(!initialNames.includes('dsh_im_return_file'), 'initial model call exposes plugin-provided dsh_im_return_file')
|
|
392
|
+
assert(!searchedNames.includes('fixture_echo'), 'searching dsh_im_return_file exposes unrelated fixture_echo')
|
|
393
|
+
assert(searchedNames.includes('dsh_im_return_file'), 'next model call omits selected dsh_im_return_file')
|
|
345
394
|
const searchResult = toolResultTexts(llm.requests[1]).at(-1) ?? ''
|
|
346
|
-
assert(searchResult.includes('
|
|
395
|
+
assert(searchResult.includes('dsh_im_return_file'), 'tool_search result omits dsh_im_return_file summary')
|
|
347
396
|
assert(!searchResult.includes('"parameters"'), 'tool_search result duplicates a full schema')
|
|
348
|
-
const
|
|
349
|
-
assert(
|
|
397
|
+
const returnFileResult = toolResultTexts(llm.requests[2]).at(-1) ?? ''
|
|
398
|
+
assert(returnFileResult.includes('"sent":true'), 'dsh_im_return_file did not execute through the real Agent loop')
|
|
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
|
+
|
|
350
505
|
|
|
351
506
|
process.stdout.write(`${JSON.stringify({
|
|
352
507
|
ok: true,
|
|
@@ -356,8 +511,11 @@ try {
|
|
|
356
511
|
modelCalls: llm.requests.length,
|
|
357
512
|
initialToolCount: initialNames.length,
|
|
358
513
|
selectedToolCount: searchedNames.length,
|
|
359
|
-
selectedOnNextCall: searchedNames.includes('
|
|
360
|
-
|
|
514
|
+
selectedOnNextCall: searchedNames.includes('dsh_im_return_file'),
|
|
515
|
+
presentation,
|
|
516
|
+
modeRejection,
|
|
517
|
+
dshScript,
|
|
518
|
+
package: `@anionex/dsh-tool-search@${packageVersion}`,
|
|
361
519
|
temporaryHome: keepTemp ? home : undefined,
|
|
362
520
|
}, null, 2)}\n`)
|
|
363
521
|
} finally {
|
package/scripts/validate.mjs
CHANGED
|
@@ -24,7 +24,7 @@ async function exists(path) {
|
|
|
24
24
|
|
|
25
25
|
const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'))
|
|
26
26
|
check(pkg.name === '@anionex/dsh-tool-search', 'package name is incorrect')
|
|
27
|
-
check(pkg.version === '0.1.
|
|
27
|
+
check(pkg.version === '0.1.1', 'release version is incorrect')
|
|
28
28
|
check(pkg.main === './lib/index.js', 'main entrypoint is incorrect')
|
|
29
29
|
check(pkg.types === './lib/types/index.d.ts', 'types entrypoint is incorrect')
|
|
30
30
|
check(pkg.exports?.['./client']?.default === './lib/client.js', 'client export is incorrect')
|
package/src/client/index.tsx
CHANGED
|
@@ -73,6 +73,7 @@ const en = {
|
|
|
73
73
|
selected: 'Live agents',
|
|
74
74
|
defaultPolicy: 'Default',
|
|
75
75
|
alwaysPolicy: 'Always',
|
|
76
|
+
deferredPolicy: 'Deferred',
|
|
76
77
|
blockedPolicy: 'Blocked',
|
|
77
78
|
loading: 'Loading...',
|
|
78
79
|
empty: 'No matching tools',
|
|
@@ -93,6 +94,7 @@ const zh: typeof en = {
|
|
|
93
94
|
selected: '活跃 Agent',
|
|
94
95
|
defaultPolicy: '默认',
|
|
95
96
|
alwaysPolicy: '常驻',
|
|
97
|
+
deferredPolicy: '延迟',
|
|
96
98
|
blockedPolicy: '禁用',
|
|
97
99
|
loading: '加载中...',
|
|
98
100
|
empty: '没有匹配工具',
|
|
@@ -296,7 +298,9 @@ export function ToolSearchSettingsSection({ scope, t }: SettingsProps): ReactNod
|
|
|
296
298
|
disabled={!snapshot.writable || tool.name === TOOL_SEARCH_NAME}
|
|
297
299
|
onChange={event => changePolicy(tool.name, event.target.value as ToolPolicy | 'default')}
|
|
298
300
|
>
|
|
299
|
-
<option value="default">
|
|
301
|
+
<option value="default">
|
|
302
|
+
{`${translate('defaultPolicy')} (${translate(core.has(tool.name) ? 'alwaysPolicy' : 'deferredPolicy')})`}
|
|
303
|
+
</option>
|
|
300
304
|
<option value="always">{translate('alwaysPolicy')}</option>
|
|
301
305
|
<option value="blocked">{translate('blockedPolicy')}</option>
|
|
302
306
|
</select>
|
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
|
|
@@ -16,7 +30,6 @@ export const DEFAULT_CORE_TOOLS = Object.freeze([
|
|
|
16
30
|
'ask_user_question',
|
|
17
31
|
'bash',
|
|
18
32
|
'create_goal',
|
|
19
|
-
'dsh_im_return_file',
|
|
20
33
|
'exit_plan_mode',
|
|
21
34
|
'get_goal',
|
|
22
35
|
'interrupt_agent',
|
package/tests/client.spec.tsx
CHANGED
|
@@ -23,12 +23,21 @@ describe('Tool Search settings section', () => {
|
|
|
23
23
|
description: 'Capture a browser screenshot.',
|
|
24
24
|
policy: 'deferred',
|
|
25
25
|
selectedBy: [],
|
|
26
|
+
}, {
|
|
27
|
+
name: 'dsh_im_return_file',
|
|
28
|
+
description: 'Send a readable file to the user.',
|
|
29
|
+
policy: 'deferred',
|
|
30
|
+
selectedBy: [],
|
|
26
31
|
}],
|
|
27
32
|
}), { status: 200 }))))
|
|
28
33
|
|
|
29
34
|
render(<ToolSearchSettingsSection scope={scope} />)
|
|
30
35
|
expect(await screen.findByText('browser_take_screenshot')).toBeDefined()
|
|
31
|
-
|
|
36
|
+
const imPolicy = screen.getByLabelText('Policy: dsh_im_return_file') as HTMLSelectElement
|
|
37
|
+
expect(imPolicy.value).toBe('default')
|
|
38
|
+
expect(imPolicy.options[0]?.text).toBe('Default (Deferred)')
|
|
39
|
+
expect(screen.getAllByLabelText('Live agents: 0').map(element => element.getAttribute('data-label')))
|
|
40
|
+
.toEqual(['Live agents', 'Live agents'])
|
|
32
41
|
fireEvent.change(screen.getByLabelText('Policy: browser_take_screenshot'), { target: { value: 'always' } })
|
|
33
42
|
await waitFor(() => {
|
|
34
43
|
expect(set).toHaveBeenCalledWith('alwaysVisible', ['browser_take_screenshot'])
|