@tokensapi/dsh-progressive-tools 0.1.0
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 +124 -0
- package/CONTRIBUTING.md +32 -0
- package/LICENSE +22 -0
- package/README.md +246 -0
- package/README.zh-CN.md +216 -0
- package/SECURITY.md +22 -0
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/cordis.patch.yml +3 -0
- package/docs/architecture.md +225 -0
- package/docs/configuration.md +238 -0
- package/docs/progressive-disclosure.md +82 -0
- package/lib/catalog.d.ts +12 -0
- package/lib/catalog.js +267 -0
- package/lib/defaults.d.ts +16 -0
- package/lib/defaults.js +122 -0
- package/lib/index.d.ts +50 -0
- package/lib/index.js +908 -0
- package/lib/state.d.ts +16 -0
- package/lib/state.js +110 -0
- package/lib/types.d.ts +120 -0
- package/lib/types.js +1 -0
- package/package.json +96 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-stable progressive disclosure for the DeepSeek Harness tool registry.
|
|
3
|
+
*
|
|
4
|
+
* The default mode keeps one byte-stable model-facing surface and dispatches
|
|
5
|
+
* deferred tools through the ordinary Harness execution pipeline. A legacy
|
|
6
|
+
* dynamic mode remains available for deployments that require native schemas.
|
|
7
|
+
*/
|
|
8
|
+
import z from '@deepseek-ai/schemastery';
|
|
9
|
+
import { CallId, HarnessError } from '@deepseek-ai/dsh-llm';
|
|
10
|
+
import { defineTool, renderToolsSdk, renderToolsSdkPy } from '@deepseek-ai/dsh-tools';
|
|
11
|
+
import { buildCatalog, estimateSchemaTokens, matchesToolName, searchTools, } from './catalog.js';
|
|
12
|
+
import { DEFAULT_ACTIVATION_GROUP_LIMIT, DEFAULT_ALWAYS_VISIBLE, DEFAULT_CHARACTERS_PER_TOKEN, DEFAULT_DEFER_TOOL_GUIDANCE, DEFAULT_DISPATCH_TOOL_NAME, DEFAULT_GROUPS, DEFAULT_MAX_ACTIVE_GROUPS, DEFAULT_MAX_ACTIVE_TOOL_TOKENS, DEFAULT_MAX_RESULTS, DEFAULT_MODE, DEFAULT_REQUIRE_DISCOVERY, DEFAULT_RETENTION_TURNS, DEFAULT_SKILL_BINDINGS, DEFAULT_STATUS_GRANTS_DISCOVERY, DEFAULT_TOOL_NAME, } from './defaults.js';
|
|
13
|
+
import { activateGroups, createProgressiveState, expireGroups, proposeSearch, restoreSnapshot, snapshotState, touchTool, } from './state.js';
|
|
14
|
+
export { buildCatalog, estimateSchemaTokens, searchCatalog, searchTools } from './catalog.js';
|
|
15
|
+
export { activateGroups, createProgressiveState, expireGroups, proposeSearch, restoreSnapshot, snapshotState, touchTool, } from './state.js';
|
|
16
|
+
export const name = 'tokens-progressive-tools';
|
|
17
|
+
export const inject = ['tools', 'systemPrompt'];
|
|
18
|
+
const groupConfigSchema = z.object({
|
|
19
|
+
id: z.string().required(),
|
|
20
|
+
description: z.string(),
|
|
21
|
+
aliases: z.array(z.string()).default([]),
|
|
22
|
+
include: z.array(z.string()).required(),
|
|
23
|
+
exclude: z.array(z.string()).default([]),
|
|
24
|
+
});
|
|
25
|
+
const skillBindingSchema = z.object({
|
|
26
|
+
skill: z.string().required(),
|
|
27
|
+
groups: z.array(z.string()).required(),
|
|
28
|
+
});
|
|
29
|
+
export const Config = z.object({
|
|
30
|
+
mode: z.string().default(DEFAULT_MODE),
|
|
31
|
+
toolName: z.string().default(DEFAULT_TOOL_NAME),
|
|
32
|
+
dispatchToolName: z.string().default(DEFAULT_DISPATCH_TOOL_NAME),
|
|
33
|
+
alwaysVisible: z.array(z.string()).default([...DEFAULT_ALWAYS_VISIBLE]),
|
|
34
|
+
groups: z.array(groupConfigSchema).default(DEFAULT_GROUPS.map(group => ({
|
|
35
|
+
...group,
|
|
36
|
+
description: group.description ?? '',
|
|
37
|
+
aliases: [...group.aliases ?? []],
|
|
38
|
+
include: [...group.include],
|
|
39
|
+
exclude: [...group.exclude ?? []],
|
|
40
|
+
}))),
|
|
41
|
+
skillBindings: z.array(skillBindingSchema).default([]),
|
|
42
|
+
maxResults: z.number().default(DEFAULT_MAX_RESULTS),
|
|
43
|
+
activationGroupLimit: z.number().default(DEFAULT_ACTIVATION_GROUP_LIMIT),
|
|
44
|
+
maxActiveGroups: z.number().default(DEFAULT_MAX_ACTIVE_GROUPS),
|
|
45
|
+
maxActiveToolTokens: z.number().default(DEFAULT_MAX_ACTIVE_TOOL_TOKENS),
|
|
46
|
+
retentionTurns: z.number().default(DEFAULT_RETENTION_TURNS),
|
|
47
|
+
charactersPerToken: z.number().default(DEFAULT_CHARACTERS_PER_TOKEN),
|
|
48
|
+
requireDiscovery: z.boolean().default(DEFAULT_REQUIRE_DISCOVERY),
|
|
49
|
+
statusGrantsDiscovery: z.boolean().default(DEFAULT_STATUS_GRANTS_DISCOVERY),
|
|
50
|
+
deferToolGuidance: z.boolean().default(DEFAULT_DEFER_TOOL_GUIDANCE),
|
|
51
|
+
});
|
|
52
|
+
function nonEmpty(value, path) {
|
|
53
|
+
const trimmed = value.trim();
|
|
54
|
+
if (trimmed === '')
|
|
55
|
+
throw new Error(`${path} must not be empty`);
|
|
56
|
+
return trimmed;
|
|
57
|
+
}
|
|
58
|
+
function integer(value, path, minimum) {
|
|
59
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
60
|
+
throw new Error(`${path} must be a safe integer greater than or equal to ${minimum}`);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
export function resolveConfig(config = {}) {
|
|
65
|
+
const mode = config.mode ?? DEFAULT_MODE;
|
|
66
|
+
if (mode !== 'stable-proxy' && mode !== 'dynamic') {
|
|
67
|
+
throw new Error('mode must be either "stable-proxy" or "dynamic"');
|
|
68
|
+
}
|
|
69
|
+
const toolName = nonEmpty(config.toolName ?? DEFAULT_TOOL_NAME, 'toolName');
|
|
70
|
+
const dispatchToolName = nonEmpty(config.dispatchToolName ?? DEFAULT_DISPATCH_TOOL_NAME, 'dispatchToolName');
|
|
71
|
+
if (toolName === dispatchToolName)
|
|
72
|
+
throw new Error('toolName and dispatchToolName must differ');
|
|
73
|
+
const alwaysVisible = (config.alwaysVisible ?? DEFAULT_ALWAYS_VISIBLE)
|
|
74
|
+
.map((pattern, index) => nonEmpty(pattern, `alwaysVisible[${index}]`));
|
|
75
|
+
const groups = (config.groups ?? DEFAULT_GROUPS).map((group, index) => {
|
|
76
|
+
const description = group.description?.trim();
|
|
77
|
+
return {
|
|
78
|
+
id: nonEmpty(group.id, `groups[${index}].id`),
|
|
79
|
+
...(description === undefined || description === '' ? {} : { description }),
|
|
80
|
+
aliases: (group.aliases ?? []).map((alias, aliasIndex) => nonEmpty(alias, `groups[${index}].aliases[${aliasIndex}]`)),
|
|
81
|
+
include: group.include.map((pattern, patternIndex) => nonEmpty(pattern, `groups[${index}].include[${patternIndex}]`)),
|
|
82
|
+
exclude: (group.exclude ?? []).map((pattern, patternIndex) => nonEmpty(pattern, `groups[${index}].exclude[${patternIndex}]`)),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
const groupIds = new Set();
|
|
86
|
+
for (const [index, group] of groups.entries()) {
|
|
87
|
+
if (group.include.length === 0)
|
|
88
|
+
throw new Error(`groups[${index}].include must not be empty`);
|
|
89
|
+
if (groupIds.has(group.id))
|
|
90
|
+
throw new Error(`duplicate group id ${JSON.stringify(group.id)}`);
|
|
91
|
+
groupIds.add(group.id);
|
|
92
|
+
}
|
|
93
|
+
const skillBindings = (config.skillBindings ?? DEFAULT_SKILL_BINDINGS).map((binding, index) => {
|
|
94
|
+
const skill = nonEmpty(binding.skill, `skillBindings[${index}].skill`);
|
|
95
|
+
if (binding.groups.length === 0)
|
|
96
|
+
throw new Error(`skillBindings[${index}].groups must not be empty`);
|
|
97
|
+
const boundGroups = binding.groups.map((group, groupIndex) => nonEmpty(group, `skillBindings[${index}].groups[${groupIndex}]`));
|
|
98
|
+
for (const group of boundGroups) {
|
|
99
|
+
if (!groupIds.has(group)) {
|
|
100
|
+
throw new Error(`skill binding ${JSON.stringify(skill)} names unknown group ${JSON.stringify(group)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { skill, groups: boundGroups };
|
|
104
|
+
});
|
|
105
|
+
const maxResults = integer(config.maxResults ?? DEFAULT_MAX_RESULTS, 'maxResults', 1);
|
|
106
|
+
const activationGroupLimit = integer(config.activationGroupLimit ?? DEFAULT_ACTIVATION_GROUP_LIMIT, 'activationGroupLimit', 1);
|
|
107
|
+
const maxActiveGroups = integer(config.maxActiveGroups ?? DEFAULT_MAX_ACTIVE_GROUPS, 'maxActiveGroups', 1);
|
|
108
|
+
if (activationGroupLimit > maxActiveGroups) {
|
|
109
|
+
throw new Error('activationGroupLimit must not exceed maxActiveGroups');
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
mode,
|
|
113
|
+
toolName,
|
|
114
|
+
dispatchToolName,
|
|
115
|
+
alwaysVisible,
|
|
116
|
+
groups,
|
|
117
|
+
skillBindings,
|
|
118
|
+
maxResults,
|
|
119
|
+
activationGroupLimit,
|
|
120
|
+
maxActiveGroups,
|
|
121
|
+
maxActiveToolTokens: integer(config.maxActiveToolTokens ?? DEFAULT_MAX_ACTIVE_TOOL_TOKENS, 'maxActiveToolTokens', 1),
|
|
122
|
+
retentionTurns: integer(config.retentionTurns ?? DEFAULT_RETENTION_TURNS, 'retentionTurns', 0),
|
|
123
|
+
charactersPerToken: integer(config.charactersPerToken ?? DEFAULT_CHARACTERS_PER_TOKEN, 'charactersPerToken', 1),
|
|
124
|
+
requireDiscovery: config.requireDiscovery ?? DEFAULT_REQUIRE_DISCOVERY,
|
|
125
|
+
statusGrantsDiscovery: config.statusGrantsDiscovery ?? DEFAULT_STATUS_GRANTS_DISCOVERY,
|
|
126
|
+
deferToolGuidance: config.deferToolGuidance ?? DEFAULT_DEFER_TOOL_GUIDANCE,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function isRecord(value) {
|
|
130
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
131
|
+
}
|
|
132
|
+
function parseJson(value) {
|
|
133
|
+
if (typeof value !== 'string')
|
|
134
|
+
return value;
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(value);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function parseActiveGroup(value) {
|
|
143
|
+
if (!isRecord(value)
|
|
144
|
+
|| typeof value.id !== 'string'
|
|
145
|
+
|| !Number.isSafeInteger(value.activatedAtTurn)
|
|
146
|
+
|| !Number.isSafeInteger(value.lastUsedTurn))
|
|
147
|
+
return undefined;
|
|
148
|
+
return {
|
|
149
|
+
id: value.id,
|
|
150
|
+
activatedAtTurn: value.activatedAtTurn,
|
|
151
|
+
lastUsedTurn: value.lastUsedTurn,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function parseSnapshot(value) {
|
|
155
|
+
if (!isRecord(value) || !Array.isArray(value.activeGroups))
|
|
156
|
+
return undefined;
|
|
157
|
+
const activeGroups = [];
|
|
158
|
+
for (const candidate of value.activeGroups) {
|
|
159
|
+
const parsed = parseActiveGroup(candidate);
|
|
160
|
+
if (parsed === undefined)
|
|
161
|
+
return undefined;
|
|
162
|
+
activeGroups.push(parsed);
|
|
163
|
+
}
|
|
164
|
+
return { activeGroups };
|
|
165
|
+
}
|
|
166
|
+
function snapshotFromSearchValue(value) {
|
|
167
|
+
if (!isRecord(value) || value.protocol !== 'dsh-progressive-tools/v1')
|
|
168
|
+
return undefined;
|
|
169
|
+
return parseSnapshot(value.state);
|
|
170
|
+
}
|
|
171
|
+
function discoveredFromSearchValue(value) {
|
|
172
|
+
if (!isRecord(value) || value.protocol !== 'dsh-progressive-tools/v2')
|
|
173
|
+
return undefined;
|
|
174
|
+
// Cumulative lists (older results, presentation meta) take priority; newer
|
|
175
|
+
// rendered results carry per-call increments that union across events.
|
|
176
|
+
if (Array.isArray(value.allDiscoveredTools)
|
|
177
|
+
&& value.allDiscoveredTools.every(name => typeof name === 'string')) {
|
|
178
|
+
return value.allDiscoveredTools;
|
|
179
|
+
}
|
|
180
|
+
if (Array.isArray(value.discoveredTools)
|
|
181
|
+
&& value.discoveredTools.every(name => typeof name === 'string')) {
|
|
182
|
+
return value.discoveredTools;
|
|
183
|
+
}
|
|
184
|
+
if (!Array.isArray(value.matches))
|
|
185
|
+
return undefined;
|
|
186
|
+
const names = value.matches
|
|
187
|
+
.map(match => isRecord(match) && typeof match.name === 'string' ? match.name : undefined)
|
|
188
|
+
.filter((name) => name !== undefined);
|
|
189
|
+
return names;
|
|
190
|
+
}
|
|
191
|
+
function statusFromSearchValue(value) {
|
|
192
|
+
return isRecord(value)
|
|
193
|
+
&& value.protocol === 'dsh-progressive-tools/v2'
|
|
194
|
+
&& value.action === 'status';
|
|
195
|
+
}
|
|
196
|
+
function textContentValue(content) {
|
|
197
|
+
if (!Array.isArray(content))
|
|
198
|
+
return undefined;
|
|
199
|
+
const first = content[0];
|
|
200
|
+
return isRecord(first) && first.type === 'text' ? parseJson(first.text) : undefined;
|
|
201
|
+
}
|
|
202
|
+
function toolResultContent(message) {
|
|
203
|
+
if (!isRecord(message) || !isRecord(message.source) || typeof message.source.callId !== 'string')
|
|
204
|
+
return undefined;
|
|
205
|
+
if (!Array.isArray(message.content) || !isRecord(message.content[0]))
|
|
206
|
+
return undefined;
|
|
207
|
+
const block = message.content[0];
|
|
208
|
+
if (block.type !== 'tool-result')
|
|
209
|
+
return undefined;
|
|
210
|
+
return {
|
|
211
|
+
callId: message.source.callId,
|
|
212
|
+
isError: block.isError === true,
|
|
213
|
+
value: textContentValue(block.content),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function skillNameFromArguments(value) {
|
|
217
|
+
const parsed = parseJson(value);
|
|
218
|
+
if (!isRecord(parsed))
|
|
219
|
+
return undefined;
|
|
220
|
+
if (typeof parsed.name === 'string')
|
|
221
|
+
return parsed.name;
|
|
222
|
+
if (typeof parsed.skill === 'string')
|
|
223
|
+
return parsed.skill;
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
function eventTurn(event) {
|
|
227
|
+
if (!isRecord(event) || !isRecord(event.data) || !Number.isSafeInteger(event.data.turn))
|
|
228
|
+
return 0;
|
|
229
|
+
return event.data.turn;
|
|
230
|
+
}
|
|
231
|
+
function cloneSchemas(value) {
|
|
232
|
+
return value.map(schema => ({
|
|
233
|
+
name: schema.name,
|
|
234
|
+
description: schema.description,
|
|
235
|
+
parameters: schema.parameters,
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
function legacyStateMeta(value) {
|
|
239
|
+
return {
|
|
240
|
+
protocol: value.protocol,
|
|
241
|
+
state: value.state,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function proxyStateMeta(value) {
|
|
245
|
+
// Presentation meta never reaches the model, so it can afford the cumulative
|
|
246
|
+
// list: resume restores the full discovery state from the latest entry even
|
|
247
|
+
// when older events were compacted away.
|
|
248
|
+
return {
|
|
249
|
+
protocol: value.protocol,
|
|
250
|
+
action: value.action,
|
|
251
|
+
discoveredTools: [...value.allDiscoveredTools],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const activeGroupSchema = {
|
|
255
|
+
type: 'object',
|
|
256
|
+
additionalProperties: false,
|
|
257
|
+
properties: {
|
|
258
|
+
id: { type: 'string', required: true },
|
|
259
|
+
activatedAtTurn: { type: 'integer', required: true },
|
|
260
|
+
lastUsedTurn: { type: 'integer', required: true },
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
const legacySearchResultSchema = {
|
|
264
|
+
type: 'object',
|
|
265
|
+
additionalProperties: false,
|
|
266
|
+
properties: {
|
|
267
|
+
protocol: { type: 'string', enum: ['dsh-progressive-tools/v1'], required: true },
|
|
268
|
+
action: { type: 'string', enum: ['search', 'status', 'reset'], required: true },
|
|
269
|
+
query: { type: 'string', required: true },
|
|
270
|
+
matches: {
|
|
271
|
+
type: 'array',
|
|
272
|
+
required: true,
|
|
273
|
+
items: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
additionalProperties: false,
|
|
276
|
+
properties: {
|
|
277
|
+
group: { type: 'string', required: true },
|
|
278
|
+
description: { type: 'string', required: true },
|
|
279
|
+
score: { type: 'number', required: true },
|
|
280
|
+
estimatedTokens: { type: 'integer', required: true },
|
|
281
|
+
tools: { type: 'array', items: { type: 'string' }, required: true },
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
activatedGroups: { type: 'array', items: { type: 'string' }, required: true },
|
|
286
|
+
evictedGroups: { type: 'array', items: { type: 'string' }, required: true },
|
|
287
|
+
activeGroups: { type: 'array', items: { type: 'string' }, required: true },
|
|
288
|
+
activeTools: { type: 'array', items: { type: 'string' }, required: true },
|
|
289
|
+
estimatedActiveTokens: { type: 'integer', required: true },
|
|
290
|
+
estimatedCatalogTokens: { type: 'integer', required: true },
|
|
291
|
+
estimatedSavedTokens: { type: 'integer', required: true },
|
|
292
|
+
catalogTools: { type: 'integer', required: true },
|
|
293
|
+
state: {
|
|
294
|
+
type: 'object',
|
|
295
|
+
additionalProperties: false,
|
|
296
|
+
required: true,
|
|
297
|
+
properties: {
|
|
298
|
+
activeGroups: { type: 'array', items: activeGroupSchema, required: true },
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
const proxyResultSchema = {
|
|
304
|
+
type: 'object',
|
|
305
|
+
additionalProperties: true,
|
|
306
|
+
};
|
|
307
|
+
function legacyResultFromExecution(result) {
|
|
308
|
+
if (result.isError || !isRecord(result.value) || result.value.protocol !== 'dsh-progressive-tools/v1') {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
const snapshot = parseSnapshot(result.value.state);
|
|
312
|
+
if (snapshot === undefined)
|
|
313
|
+
return undefined;
|
|
314
|
+
return result.value;
|
|
315
|
+
}
|
|
316
|
+
function proxyResultFromExecution(result) {
|
|
317
|
+
if (result.isError || !isRecord(result.value) || result.value.protocol !== 'dsh-progressive-tools/v2') {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
return result.value;
|
|
321
|
+
}
|
|
322
|
+
function proxyContent(value) {
|
|
323
|
+
if (!isRecord(value) || !Array.isArray(value.content)) {
|
|
324
|
+
return [{ type: 'text', text: JSON.stringify(value) }];
|
|
325
|
+
}
|
|
326
|
+
return value.content;
|
|
327
|
+
}
|
|
328
|
+
function exactGuidanceForDeferredTool(sectionName, deferredNames) {
|
|
329
|
+
if (!sectionName.startsWith('tool:') || sectionName === 'tools:sdk' || sectionName === 'tools:code-only') {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
const suffix = sectionName.slice('tool:'.length);
|
|
333
|
+
for (const name of deferredNames) {
|
|
334
|
+
if (suffix === name || suffix.startsWith(`${name}:`))
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
export function apply(ctx, input) {
|
|
340
|
+
const config = resolveConfig(input);
|
|
341
|
+
const states = new WeakMap();
|
|
342
|
+
const liveStates = new Set();
|
|
343
|
+
const skillBindings = new Map(config.skillBindings.map(binding => [binding.skill, binding.groups]));
|
|
344
|
+
const authorizedProxyParents = new Set();
|
|
345
|
+
let restrictionMutationDepth = 0;
|
|
346
|
+
const mutateRestriction = (operation) => {
|
|
347
|
+
restrictionMutationDepth += 1;
|
|
348
|
+
try {
|
|
349
|
+
return operation();
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
restrictionMutationDepth -= 1;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
const disposeRestriction = (state) => {
|
|
356
|
+
const dispose = state.restriction;
|
|
357
|
+
state.restriction = undefined;
|
|
358
|
+
if (dispose !== undefined)
|
|
359
|
+
mutateRestriction(dispose);
|
|
360
|
+
};
|
|
361
|
+
const latestTurn = (agent) => agent.session.events.reduce((maximum, event) => Math.max(maximum, eventTurn(event)), 0);
|
|
362
|
+
const ensureState = (agent) => {
|
|
363
|
+
const existing = states.get(agent);
|
|
364
|
+
if (existing !== undefined)
|
|
365
|
+
return existing;
|
|
366
|
+
const created = {
|
|
367
|
+
agent,
|
|
368
|
+
progressive: createProgressiveState(buildCatalog([], config.groups, config.charactersPerToken), latestTurn(agent)),
|
|
369
|
+
discovered: new Set(),
|
|
370
|
+
catalogListed: false,
|
|
371
|
+
restriction: undefined,
|
|
372
|
+
restrictableNames: new Set(),
|
|
373
|
+
eagerNames: new Set(),
|
|
374
|
+
stableNames: undefined,
|
|
375
|
+
catalogDirty: true,
|
|
376
|
+
restored: false,
|
|
377
|
+
};
|
|
378
|
+
states.set(agent, created);
|
|
379
|
+
liveStates.add(created);
|
|
380
|
+
return created;
|
|
381
|
+
};
|
|
382
|
+
const discoverGroups = (state, groups) => {
|
|
383
|
+
for (const groupId of groups) {
|
|
384
|
+
const group = state.progressive.catalog.groups.get(groupId);
|
|
385
|
+
if (group === undefined)
|
|
386
|
+
continue;
|
|
387
|
+
for (const tool of group.tools)
|
|
388
|
+
state.discovered.add(tool.name);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const applySkillBinding = (state, argumentsValue, turn) => {
|
|
392
|
+
const skillName = skillNameFromArguments(argumentsValue);
|
|
393
|
+
if (skillName === undefined)
|
|
394
|
+
return;
|
|
395
|
+
const groups = skillBindings.get(skillName);
|
|
396
|
+
if (groups === undefined)
|
|
397
|
+
return;
|
|
398
|
+
if (config.mode === 'stable-proxy')
|
|
399
|
+
discoverGroups(state, groups);
|
|
400
|
+
else
|
|
401
|
+
activateGroups(state.progressive, groups, turn, config);
|
|
402
|
+
};
|
|
403
|
+
const restoreFromEvents = (state) => {
|
|
404
|
+
const calls = new Map();
|
|
405
|
+
for (const event of state.agent.session.events) {
|
|
406
|
+
state.progressive.currentTurn = Math.max(state.progressive.currentTurn, eventTurn(event));
|
|
407
|
+
if (event.type === 'tool/call') {
|
|
408
|
+
calls.set(String(event.data.callId), {
|
|
409
|
+
name: event.data.name,
|
|
410
|
+
arguments: event.data.arguments,
|
|
411
|
+
turn: event.data.turn,
|
|
412
|
+
});
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (event.type === 'tool/result') {
|
|
416
|
+
const result = toolResultContent(event.data.message);
|
|
417
|
+
if (result === undefined || result.isError)
|
|
418
|
+
continue;
|
|
419
|
+
const call = calls.get(result.callId);
|
|
420
|
+
if (call === undefined)
|
|
421
|
+
continue;
|
|
422
|
+
if (call.name === config.toolName) {
|
|
423
|
+
if (config.mode === 'stable-proxy') {
|
|
424
|
+
const meta = isRecord(event.data.meta) ? event.data.meta : undefined;
|
|
425
|
+
const fromMeta = meta === undefined ? undefined : discoveredFromSearchValue(meta);
|
|
426
|
+
for (const name of fromMeta ?? discoveredFromSearchValue(result.value) ?? []) {
|
|
427
|
+
state.discovered.add(name);
|
|
428
|
+
}
|
|
429
|
+
if (statusFromSearchValue(meta) || statusFromSearchValue(result.value)) {
|
|
430
|
+
state.catalogListed = true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
const snapshot = isRecord(event.data.meta)
|
|
435
|
+
? snapshotFromSearchValue({ protocol: event.data.meta.protocol, state: event.data.meta.state })
|
|
436
|
+
: undefined;
|
|
437
|
+
const fallback = snapshotFromSearchValue(result.value);
|
|
438
|
+
if (snapshot !== undefined || fallback !== undefined) {
|
|
439
|
+
restoreSnapshot(state.progressive, snapshot ?? fallback);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
else if (call.name === 'skill') {
|
|
444
|
+
applySkillBinding(state, call.arguments, call.turn);
|
|
445
|
+
}
|
|
446
|
+
else if (config.mode === 'dynamic') {
|
|
447
|
+
touchTool(state.progressive, call.name, call.turn);
|
|
448
|
+
}
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (event.type !== 'tool/code-dispatch')
|
|
452
|
+
continue;
|
|
453
|
+
const nested = event.data;
|
|
454
|
+
if (nested.isError)
|
|
455
|
+
continue;
|
|
456
|
+
if (nested.name === config.toolName) {
|
|
457
|
+
const value = textContentValue(nested.content);
|
|
458
|
+
if (config.mode === 'stable-proxy') {
|
|
459
|
+
for (const name of discoveredFromSearchValue(value) ?? [])
|
|
460
|
+
state.discovered.add(name);
|
|
461
|
+
if (statusFromSearchValue(value))
|
|
462
|
+
state.catalogListed = true;
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
const snapshot = snapshotFromSearchValue(value);
|
|
466
|
+
if (snapshot !== undefined)
|
|
467
|
+
restoreSnapshot(state.progressive, snapshot);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else if (nested.name === 'skill') {
|
|
471
|
+
applySkillBinding(state, nested.arguments, eventTurn(event));
|
|
472
|
+
}
|
|
473
|
+
else if (config.mode === 'dynamic') {
|
|
474
|
+
touchTool(state.progressive, nested.name, eventTurn(event));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
const rebuildStableCatalog = (state) => {
|
|
479
|
+
const schemas = cloneSchemas(state.agent.ctx.tools.schemas(state.agent));
|
|
480
|
+
if (state.stableNames === undefined) {
|
|
481
|
+
state.stableNames = new Set(schemas
|
|
482
|
+
.filter(schema => schema.name === config.toolName
|
|
483
|
+
|| schema.name === config.dispatchToolName
|
|
484
|
+
|| matchesToolName(schema.name, config.alwaysVisible))
|
|
485
|
+
.map(schema => schema.name));
|
|
486
|
+
}
|
|
487
|
+
const managed = schemas.filter(schema => schema.name !== 'run_code' && !state.stableNames.has(schema.name));
|
|
488
|
+
state.progressive.catalog = buildCatalog(managed, config.groups, config.charactersPerToken);
|
|
489
|
+
// Discovered names deliberately survive registry refreshes (for example a
|
|
490
|
+
// provider reconnect); dispatch validates catalog membership at call time.
|
|
491
|
+
if (!state.restored) {
|
|
492
|
+
restoreFromEvents(state);
|
|
493
|
+
state.restored = true;
|
|
494
|
+
}
|
|
495
|
+
state.catalogDirty = false;
|
|
496
|
+
};
|
|
497
|
+
const rebuildDynamicCatalog = (state) => {
|
|
498
|
+
const previous = snapshotState(state.progressive);
|
|
499
|
+
disposeRestriction(state);
|
|
500
|
+
const unrestricted = cloneSchemas(state.agent.ctx.tools.schemas(state.agent));
|
|
501
|
+
const hideInherited = mutateRestriction(() => state.agent.ctx.tools.restrict({ allow: [] }));
|
|
502
|
+
let ownNames;
|
|
503
|
+
try {
|
|
504
|
+
ownNames = new Set(state.agent.ctx.tools.schemas(state.agent).map(schema => schema.name));
|
|
505
|
+
}
|
|
506
|
+
finally {
|
|
507
|
+
mutateRestriction(hideInherited);
|
|
508
|
+
}
|
|
509
|
+
const restrictable = unrestricted.filter(schema => !ownNames.has(schema.name));
|
|
510
|
+
const eagerNames = new Set(restrictable
|
|
511
|
+
.filter(schema => schema.name === config.toolName || matchesToolName(schema.name, config.alwaysVisible))
|
|
512
|
+
.map(schema => schema.name));
|
|
513
|
+
const managed = restrictable.filter(schema => !eagerNames.has(schema.name));
|
|
514
|
+
state.progressive.catalog = buildCatalog(managed, config.groups, config.charactersPerToken);
|
|
515
|
+
state.restrictableNames = new Set(restrictable.map(schema => schema.name));
|
|
516
|
+
state.eagerNames = eagerNames;
|
|
517
|
+
if (state.restored)
|
|
518
|
+
restoreSnapshot(state.progressive, previous);
|
|
519
|
+
else {
|
|
520
|
+
restoreFromEvents(state);
|
|
521
|
+
state.restored = true;
|
|
522
|
+
}
|
|
523
|
+
state.catalogDirty = false;
|
|
524
|
+
};
|
|
525
|
+
const installDynamicRestriction = (state) => {
|
|
526
|
+
disposeRestriction(state);
|
|
527
|
+
const allow = new Set(state.eagerNames);
|
|
528
|
+
for (const groupId of state.progressive.active.keys()) {
|
|
529
|
+
const group = state.progressive.catalog.groups.get(groupId);
|
|
530
|
+
if (group === undefined)
|
|
531
|
+
continue;
|
|
532
|
+
for (const tool of group.tools) {
|
|
533
|
+
if (state.restrictableNames.has(tool.name))
|
|
534
|
+
allow.add(tool.name);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
state.restriction = mutateRestriction(() => state.agent.ctx.tools.restrict({ allow: [...allow].sort() }));
|
|
538
|
+
};
|
|
539
|
+
const prepareStableState = (agent) => {
|
|
540
|
+
const state = ensureState(agent);
|
|
541
|
+
if (state.catalogDirty)
|
|
542
|
+
rebuildStableCatalog(state);
|
|
543
|
+
return state;
|
|
544
|
+
};
|
|
545
|
+
const prepareDynamicState = (agent, turn) => {
|
|
546
|
+
const state = ensureState(agent);
|
|
547
|
+
state.progressive.currentTurn = Math.max(state.progressive.currentTurn, turn);
|
|
548
|
+
if (state.catalogDirty)
|
|
549
|
+
rebuildDynamicCatalog(state);
|
|
550
|
+
expireGroups(state.progressive, state.progressive.currentTurn, config);
|
|
551
|
+
installDynamicRestriction(state);
|
|
552
|
+
return state;
|
|
553
|
+
};
|
|
554
|
+
const clampLimit = (requested) => {
|
|
555
|
+
if (requested === undefined)
|
|
556
|
+
return config.maxResults;
|
|
557
|
+
return Math.min(Math.max(requested, 1), config.maxResults);
|
|
558
|
+
};
|
|
559
|
+
const deferredGroupSummaries = (state) => [...state.progressive.catalog.groups.values()]
|
|
560
|
+
.map(group => ({
|
|
561
|
+
id: group.id,
|
|
562
|
+
description: group.description.slice(0, 180),
|
|
563
|
+
tools: group.tools.map(tool => tool.name),
|
|
564
|
+
}))
|
|
565
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
566
|
+
const stableSearchResult = (state, action, query, matches) => {
|
|
567
|
+
// A search discovers the matched tools and their whole families, so one
|
|
568
|
+
// query opens a plugin's full surface instead of only its top-ranked slice.
|
|
569
|
+
const newlyDiscovered = new Set();
|
|
570
|
+
for (const match of matches) {
|
|
571
|
+
if (!state.discovered.has(match.name))
|
|
572
|
+
newlyDiscovered.add(match.name);
|
|
573
|
+
for (const sibling of match.groupTools) {
|
|
574
|
+
if (!state.discovered.has(sibling))
|
|
575
|
+
newlyDiscovered.add(sibling);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const allDiscovered = new Set(state.discovered);
|
|
579
|
+
for (const name of newlyDiscovered)
|
|
580
|
+
allDiscovered.add(name);
|
|
581
|
+
const stableSchemas = cloneSchemas(state.agent.ctx.tools.schemas(state.agent))
|
|
582
|
+
.filter(schema => state.stableNames?.has(schema.name));
|
|
583
|
+
const estimatedVisibleTokens = stableSchemas.reduce((total, schema) => total + estimateSchemaTokens(schema, config.charactersPerToken), 0);
|
|
584
|
+
return {
|
|
585
|
+
protocol: 'dsh-progressive-tools/v2',
|
|
586
|
+
mode: 'stable-proxy',
|
|
587
|
+
action,
|
|
588
|
+
query,
|
|
589
|
+
matches,
|
|
590
|
+
...(action === 'status' ? { groups: deferredGroupSummaries(state) } : {}),
|
|
591
|
+
stableTools: [...state.stableNames ?? []].sort(),
|
|
592
|
+
discoveredTools: [...newlyDiscovered].sort(),
|
|
593
|
+
discoveredCount: allDiscovered.size,
|
|
594
|
+
allDiscoveredTools: [...allDiscovered].sort(),
|
|
595
|
+
catalogTools: state.progressive.catalog.tools.size,
|
|
596
|
+
estimatedVisibleTokens,
|
|
597
|
+
estimatedCatalogTokens: state.progressive.catalog.totalEstimatedTokens,
|
|
598
|
+
estimatedSavedTokens: state.progressive.catalog.totalEstimatedTokens,
|
|
599
|
+
instruction: action === 'search'
|
|
600
|
+
? `Call ${config.dispatchToolName} with an exact returned name and arguments matching its parameters schema.`
|
|
601
|
+
: `Use ${config.toolName} with a task-oriented query to load exact deferred definitions.`,
|
|
602
|
+
};
|
|
603
|
+
};
|
|
604
|
+
if (config.mode === 'stable-proxy') {
|
|
605
|
+
ctx.tools.register(defineTool({
|
|
606
|
+
name: config.toolName,
|
|
607
|
+
description: `Search deferred tools by capability. Use this whenever the visible tools do not cover the task. Returns exact names, descriptions, and parameter schemas for ${config.dispatchToolName}.`,
|
|
608
|
+
parameters: {
|
|
609
|
+
query: {
|
|
610
|
+
type: 'string',
|
|
611
|
+
description: 'Task-oriented capability query. Include the object, action, or service involved.',
|
|
612
|
+
},
|
|
613
|
+
action: {
|
|
614
|
+
type: 'string',
|
|
615
|
+
enum: ['search', 'status'],
|
|
616
|
+
description: `Search definitions, or use status to list every deferred family and catalog estimates. Defaults to search.${config.statusGrantsDiscovery ? ' Status also makes every listed name dispatchable.' : ''}`,
|
|
617
|
+
},
|
|
618
|
+
max_results: {
|
|
619
|
+
type: 'integer',
|
|
620
|
+
description: `Maximum exact tool definitions to return; values are clamped between 1 and ${config.maxResults}.`,
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
output: {
|
|
624
|
+
schema: proxyResultSchema,
|
|
625
|
+
render: (_args, value) => {
|
|
626
|
+
// The cumulative list lives in presentation meta only; rendering it
|
|
627
|
+
// would leak the ever-growing discovery table back into the prompt.
|
|
628
|
+
const rendered = { ...value };
|
|
629
|
+
delete rendered.allDiscoveredTools;
|
|
630
|
+
return [{ type: 'text', text: JSON.stringify(rendered) }];
|
|
631
|
+
},
|
|
632
|
+
presentationMeta: (_args, value) => proxyStateMeta(value),
|
|
633
|
+
},
|
|
634
|
+
isConcurrencySafe: () => true,
|
|
635
|
+
async execute(args, exec) {
|
|
636
|
+
if (exec.agent === undefined)
|
|
637
|
+
throw new Error(`${config.toolName} requires an agent-scoped execution`);
|
|
638
|
+
const state = prepareStableState(exec.agent);
|
|
639
|
+
const action = args.action ?? 'search';
|
|
640
|
+
const query = args.query?.trim() ?? '';
|
|
641
|
+
if (action === 'search' && query === '')
|
|
642
|
+
throw new Error('query is required when action is search');
|
|
643
|
+
const matches = action === 'search'
|
|
644
|
+
? searchTools(state.progressive.catalog, query, clampLimit(args.max_results))
|
|
645
|
+
: [];
|
|
646
|
+
return stableSearchResult(state, action, action === 'search' ? query : '', matches);
|
|
647
|
+
},
|
|
648
|
+
}));
|
|
649
|
+
ctx.tools.register(defineTool({
|
|
650
|
+
name: config.dispatchToolName,
|
|
651
|
+
description: `Execute one exact tool returned by ${config.toolName}. Copy the returned name exactly and pass arguments that satisfy its parameters schema.`,
|
|
652
|
+
parameters: {
|
|
653
|
+
name: {
|
|
654
|
+
type: 'string',
|
|
655
|
+
required: true,
|
|
656
|
+
description: `Exact tool name returned by ${config.toolName}.`,
|
|
657
|
+
},
|
|
658
|
+
arguments: {
|
|
659
|
+
type: 'object',
|
|
660
|
+
additionalProperties: true,
|
|
661
|
+
required: true,
|
|
662
|
+
description: 'Arguments matching the selected tool parameters schema.',
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
output: {
|
|
666
|
+
schema: proxyResultSchema,
|
|
667
|
+
render: (_args, value) => proxyContent(value),
|
|
668
|
+
presentationMeta: (args) => ({
|
|
669
|
+
protocol: 'dsh-progressive-tools/dispatch-v1',
|
|
670
|
+
tool: args.name,
|
|
671
|
+
}),
|
|
672
|
+
},
|
|
673
|
+
// Parallel scheduling follows the real tool's own classifier so deferred
|
|
674
|
+
// tools keep the concurrency they declare; unknown targets stay exclusive.
|
|
675
|
+
isConcurrencySafe(args) {
|
|
676
|
+
const definition = ctx.tools.get(args.name);
|
|
677
|
+
if (definition?.isConcurrencySafe === undefined)
|
|
678
|
+
return false;
|
|
679
|
+
try {
|
|
680
|
+
return definition.isConcurrencySafe(args.arguments) === true;
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
async execute(args, exec) {
|
|
687
|
+
if (exec.agent === undefined)
|
|
688
|
+
throw new Error(`${config.dispatchToolName} requires an agent-scoped execution`);
|
|
689
|
+
const state = prepareStableState(exec.agent);
|
|
690
|
+
if (!state.progressive.catalog.tools.has(args.name)) {
|
|
691
|
+
if (state.stableNames?.has(args.name)) {
|
|
692
|
+
throw new Error(`tool ${JSON.stringify(args.name)} is already visible and should be called directly`);
|
|
693
|
+
}
|
|
694
|
+
throw new Error(`tool ${JSON.stringify(args.name)} is not in the deferred catalog`);
|
|
695
|
+
}
|
|
696
|
+
if (config.requireDiscovery
|
|
697
|
+
&& !state.discovered.has(args.name)
|
|
698
|
+
&& !(config.statusGrantsDiscovery && state.catalogListed)) {
|
|
699
|
+
throw new Error(`tool ${JSON.stringify(args.name)} has not been discovered; call ${config.toolName} with the exact name ${JSON.stringify(args.name)} as the query to load its schema, then dispatch`);
|
|
700
|
+
}
|
|
701
|
+
const definition = exec.agent.ctx.tools.get(args.name, exec.agent);
|
|
702
|
+
if (definition === undefined)
|
|
703
|
+
throw new Error(`tool ${JSON.stringify(args.name)} is no longer registered`);
|
|
704
|
+
authorizedProxyParents.add(exec.token);
|
|
705
|
+
try {
|
|
706
|
+
const nested = await exec.agent.ctx.tools.execute({
|
|
707
|
+
signal: exec.signal,
|
|
708
|
+
callId: CallId(`${String(exec.callId)}:dispatch`),
|
|
709
|
+
rootCallId: exec.rootCallId,
|
|
710
|
+
parent: exec.token,
|
|
711
|
+
name: args.name,
|
|
712
|
+
arguments: args.arguments,
|
|
713
|
+
agent: exec.agent,
|
|
714
|
+
});
|
|
715
|
+
for (const context of nested.additionalContexts ?? [])
|
|
716
|
+
exec.deferContext(context);
|
|
717
|
+
if (nested.concludesTurn === true)
|
|
718
|
+
exec.concludeTurn();
|
|
719
|
+
if (nested.isError) {
|
|
720
|
+
// A HarnessError keeps the real tool's routable failure identity
|
|
721
|
+
// instead of collapsing it into an unstructured message.
|
|
722
|
+
const failure = new HarnessError(`${args.name}: ${nested.error.message}`, nested.error.info?.code ?? 'DISPATCH_TARGET_ERROR');
|
|
723
|
+
if (nested.error.info !== undefined)
|
|
724
|
+
failure.name = nested.error.info.name;
|
|
725
|
+
throw failure;
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
protocol: 'dsh-progressive-tools/dispatch-v1',
|
|
729
|
+
tool: args.name,
|
|
730
|
+
value: nested.value,
|
|
731
|
+
content: nested.content,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
finally {
|
|
735
|
+
authorizedProxyParents.delete(exec.token);
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
}));
|
|
739
|
+
ctx.systemPrompt.section({
|
|
740
|
+
name: 'tokens-progressive-tools:discovery',
|
|
741
|
+
order: 140,
|
|
742
|
+
text: `Only the common tools are listed initially. When the task needs another capability, call ${config.toolName}; then call ${config.dispatchToolName} with an exact returned name and schema-valid arguments. Tool names mentioned elsewhere in this prompt but not listed as callable must be discovered the same way before dispatch. Use action "status" to browse the complete deferred catalog. Do not claim a capability is unavailable before searching.`,
|
|
743
|
+
});
|
|
744
|
+
ctx.tools.guard((execution) => {
|
|
745
|
+
const agent = execution.agent;
|
|
746
|
+
if (agent === undefined)
|
|
747
|
+
return undefined;
|
|
748
|
+
// Prepare lazily so calls arriving before the first assembly or
|
|
749
|
+
// session-start event are still classified against the deferred catalog.
|
|
750
|
+
const state = prepareStableState(agent);
|
|
751
|
+
if (execution.parent !== undefined && authorizedProxyParents.has(execution.parent)) {
|
|
752
|
+
authorizedProxyParents.add(execution.token);
|
|
753
|
+
return undefined;
|
|
754
|
+
}
|
|
755
|
+
if (execution.name === 'run_code' || state.stableNames?.has(execution.name))
|
|
756
|
+
return undefined;
|
|
757
|
+
if (!state.progressive.catalog.tools.has(execution.name))
|
|
758
|
+
return undefined;
|
|
759
|
+
return `tool ${JSON.stringify(execution.name)} is deferred; use ${config.toolName} and ${config.dispatchToolName}`;
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
ctx.tools.register(defineTool({
|
|
764
|
+
name: config.toolName,
|
|
765
|
+
description: 'Search the hidden tool catalog and activate only the relevant tool families. Use status to inspect active families or reset to release them.',
|
|
766
|
+
parameters: {
|
|
767
|
+
query: {
|
|
768
|
+
type: 'string',
|
|
769
|
+
description: 'Capability to find. Required for search; use task-oriented words.',
|
|
770
|
+
},
|
|
771
|
+
action: {
|
|
772
|
+
type: 'string',
|
|
773
|
+
enum: ['search', 'status', 'reset'],
|
|
774
|
+
description: 'Search activates matching families; status inspects; reset releases them.',
|
|
775
|
+
},
|
|
776
|
+
max_results: {
|
|
777
|
+
type: 'integer',
|
|
778
|
+
description: `Maximum matches to return; values are clamped between 1 and ${config.maxResults}.`,
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
output: {
|
|
782
|
+
schema: legacySearchResultSchema,
|
|
783
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
|
|
784
|
+
presentationMeta: (_args, value) => legacyStateMeta(value),
|
|
785
|
+
},
|
|
786
|
+
async execute(args, exec) {
|
|
787
|
+
if (exec.agent === undefined)
|
|
788
|
+
throw new Error(`${config.toolName} requires an agent-scoped execution`);
|
|
789
|
+
const state = prepareDynamicState(exec.agent, latestTurn(exec.agent));
|
|
790
|
+
const action = args.action ?? 'search';
|
|
791
|
+
const query = args.query?.trim() ?? '';
|
|
792
|
+
if (action === 'search' && query === '')
|
|
793
|
+
throw new Error('query is required when action is search');
|
|
794
|
+
return proposeSearch(state.progressive, action, query, clampLimit(args.max_results), config);
|
|
795
|
+
},
|
|
796
|
+
}));
|
|
797
|
+
}
|
|
798
|
+
const shapeSdkSection = (state, visibleNames, text) => {
|
|
799
|
+
const schemas = [];
|
|
800
|
+
for (const name of [...visibleNames].sort()) {
|
|
801
|
+
if (name === 'run_code')
|
|
802
|
+
continue;
|
|
803
|
+
const definition = state.agent.ctx.tools.get(name, state.agent);
|
|
804
|
+
if (definition === undefined)
|
|
805
|
+
continue;
|
|
806
|
+
schemas.push({
|
|
807
|
+
name: definition.name,
|
|
808
|
+
description: definition.description,
|
|
809
|
+
parameters: definition.parameters,
|
|
810
|
+
output: definition.output.schema,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
return text.includes('```python') ? renderToolsSdkPy(schemas) : renderToolsSdk(schemas);
|
|
814
|
+
};
|
|
815
|
+
ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
816
|
+
const resolved = await next();
|
|
817
|
+
const agent = context.agent;
|
|
818
|
+
if (agent === undefined)
|
|
819
|
+
return resolved;
|
|
820
|
+
const state = config.mode === 'stable-proxy'
|
|
821
|
+
? prepareStableState(agent)
|
|
822
|
+
: prepareDynamicState(agent, latestTurn(agent));
|
|
823
|
+
const visibleNames = config.mode === 'stable-proxy'
|
|
824
|
+
? new Set([...state.stableNames ?? [], 'run_code'])
|
|
825
|
+
: new Set(agent.ctx.tools.schemas(agent).map(schema => schema.name));
|
|
826
|
+
const deferredNames = config.mode === 'stable-proxy'
|
|
827
|
+
? new Set(state.progressive.catalog.tools.keys())
|
|
828
|
+
: new Set();
|
|
829
|
+
const sections = resolved.sections
|
|
830
|
+
.filter(section => !config.deferToolGuidance
|
|
831
|
+
|| !exactGuidanceForDeferredTool(section.name, deferredNames))
|
|
832
|
+
.map(section => section.name === 'tools:sdk'
|
|
833
|
+
? { ...section, text: shapeSdkSection(state, visibleNames, section.text) }
|
|
834
|
+
: section);
|
|
835
|
+
return {
|
|
836
|
+
...resolved,
|
|
837
|
+
sections,
|
|
838
|
+
tools: resolved.tools.filter(schema => visibleNames.has(schema.name)),
|
|
839
|
+
};
|
|
840
|
+
}, { prepend: true });
|
|
841
|
+
ctx.on('agent/session-start', ({ agent }) => {
|
|
842
|
+
if (config.mode === 'stable-proxy')
|
|
843
|
+
prepareStableState(agent);
|
|
844
|
+
else
|
|
845
|
+
prepareDynamicState(agent, latestTurn(agent));
|
|
846
|
+
}, { prepend: true });
|
|
847
|
+
ctx.on('agent/inbox/claimed', ({ agent, turn }) => {
|
|
848
|
+
if (config.mode === 'dynamic')
|
|
849
|
+
prepareDynamicState(agent, turn);
|
|
850
|
+
}, { prepend: true });
|
|
851
|
+
ctx.on('agent/pre-step', async ({ agent, turn }, next) => {
|
|
852
|
+
if (config.mode === 'dynamic')
|
|
853
|
+
prepareDynamicState(agent, turn);
|
|
854
|
+
return next();
|
|
855
|
+
}, { prepend: true });
|
|
856
|
+
ctx.on('tools/result', (exec, result) => {
|
|
857
|
+
authorizedProxyParents.delete(exec.token);
|
|
858
|
+
const agent = exec.agent;
|
|
859
|
+
if (agent === undefined || result.isError)
|
|
860
|
+
return;
|
|
861
|
+
const state = ensureState(agent);
|
|
862
|
+
if (exec.name === config.toolName) {
|
|
863
|
+
if (config.mode === 'stable-proxy') {
|
|
864
|
+
const value = proxyResultFromExecution(result);
|
|
865
|
+
for (const name of value?.discoveredTools ?? [])
|
|
866
|
+
state.discovered.add(name);
|
|
867
|
+
if (value?.action === 'status')
|
|
868
|
+
state.catalogListed = true;
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
const value = legacyResultFromExecution(result);
|
|
872
|
+
if (value !== undefined)
|
|
873
|
+
restoreSnapshot(state.progressive, value.state);
|
|
874
|
+
if (!state.catalogDirty)
|
|
875
|
+
installDynamicRestriction(state);
|
|
876
|
+
}
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (exec.name === 'skill') {
|
|
880
|
+
applySkillBinding(state, exec.arguments, state.progressive.currentTurn);
|
|
881
|
+
if (config.mode === 'dynamic' && !state.catalogDirty)
|
|
882
|
+
installDynamicRestriction(state);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (config.mode === 'dynamic')
|
|
886
|
+
touchTool(state.progressive, exec.name, state.progressive.currentTurn);
|
|
887
|
+
});
|
|
888
|
+
ctx.on('tools/change', () => {
|
|
889
|
+
if (restrictionMutationDepth > 0)
|
|
890
|
+
return;
|
|
891
|
+
for (const state of liveStates)
|
|
892
|
+
state.catalogDirty = true;
|
|
893
|
+
});
|
|
894
|
+
ctx.on('agent/disposed', ({ agent }) => {
|
|
895
|
+
const state = states.get(agent);
|
|
896
|
+
if (state === undefined)
|
|
897
|
+
return;
|
|
898
|
+
disposeRestriction(state);
|
|
899
|
+
states.delete(agent);
|
|
900
|
+
liveStates.delete(state);
|
|
901
|
+
});
|
|
902
|
+
ctx.effect(() => () => {
|
|
903
|
+
for (const state of liveStates)
|
|
904
|
+
disposeRestriction(state);
|
|
905
|
+
liveStates.clear();
|
|
906
|
+
authorizedProxyParents.clear();
|
|
907
|
+
}, 'progressive-tools.agent-state');
|
|
908
|
+
}
|