@yeaft/webchat-agent 1.0.377 → 1.0.378
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/local-runtime/version.json +1 -1
- package/package.json +1 -1
- package/yeaft/config.js +2 -0
- package/yeaft/engine.js +237 -21
- package/yeaft/prompts.js +82 -31
- package/yeaft/sessions/project-doc.js +202 -5
- package/yeaft/templates/base.md +4 -74
- package/yeaft/templates/common-rules.md +4 -108
- package/yeaft/templates/core.md +33 -0
- package/yeaft/templates/tool-guidance.md +4 -138
- package/yeaft/tools/activation.js +180 -0
- package/yeaft/tools/discover-tools.js +198 -0
- package/yeaft/tools/index.js +2 -0
- package/yeaft/tools/registry.js +25 -9
- package/yeaft/tools/types.js +1 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { COLLAB_TOOL_POLICY } from './registry.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-ins whose schemas stay visible on every provider request. Registration
|
|
5
|
+
* remains broader than exposure so legacy aliases and conditional tools can
|
|
6
|
+
* still be resolved without paying their schema cost on unrelated turns.
|
|
7
|
+
*/
|
|
8
|
+
export const ALWAYS_VISIBLE_TOOL_NAMES = Object.freeze([
|
|
9
|
+
'Skill',
|
|
10
|
+
'EnterWorktree',
|
|
11
|
+
'ExitWorktree',
|
|
12
|
+
'AskUser',
|
|
13
|
+
'DiscoverTools',
|
|
14
|
+
'WebSearch',
|
|
15
|
+
'WebFetch',
|
|
16
|
+
'Bash',
|
|
17
|
+
'FileRead',
|
|
18
|
+
'FileWrite',
|
|
19
|
+
'FileEdit',
|
|
20
|
+
'Glob',
|
|
21
|
+
'Grep',
|
|
22
|
+
'ListDir',
|
|
23
|
+
'StartPlan',
|
|
24
|
+
'TodoWrite',
|
|
25
|
+
'ViewImage',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export const BACKGROUND_TASK_TOOL_NAMES = Object.freeze([
|
|
29
|
+
'ListTasks',
|
|
30
|
+
'ReadTaskLog',
|
|
31
|
+
'CancelTask',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
export const SUB_AGENT_MANAGEMENT_TOOL_NAMES = Object.freeze([
|
|
35
|
+
'PromptAgent',
|
|
36
|
+
'WaitAgent',
|
|
37
|
+
'CloseAgent',
|
|
38
|
+
'ListAgents',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export const CONDITIONAL_BUILTIN_TOOL_NAMES = new Set([
|
|
42
|
+
'HistorySearch',
|
|
43
|
+
'DiskUsage',
|
|
44
|
+
'ApplyPatch',
|
|
45
|
+
'ListTasks',
|
|
46
|
+
'ReadTaskLog',
|
|
47
|
+
'CancelTask',
|
|
48
|
+
'SpawnAgent',
|
|
49
|
+
'PromptAgent',
|
|
50
|
+
'WaitAgent',
|
|
51
|
+
'CloseAgent',
|
|
52
|
+
'ListAgents',
|
|
53
|
+
'RouteForward',
|
|
54
|
+
'CreateWorkItem',
|
|
55
|
+
'JsRepl',
|
|
56
|
+
'NotebookEdit',
|
|
57
|
+
'ImageGeneration',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const HISTORY_INTENT_RE = /(?:\bhistory\b|\b(?:prior|previous) (?:chat|conversation|discussion)\b|\bprevious(?:ly)? discussed\b|\bwhat did we (?:decide|discuss|say|agree)\b|\b(?:our|the) (?:earlier|last) decision\b|历史|之前(?:的)?(?:对话|讨论|会话|决定)|过去(?:的)?会话|我们(?:之前|上次)(?:决定|讨论|说)了什么)/iu;
|
|
61
|
+
const DISK_INTENT_RE = /(?:\bdisk (?:usage|space|full)\b|\bstorage (?:usage|space|full)\b|\blargest director|\benospc\b|\bno space left on device\b|磁盘(?:占用|空间|已满)|存储空间|目录占用|空间不足)/iu;
|
|
62
|
+
const PATCH_INTENT_RE = /(?:\bapply (?:a )?patch\b|\bunified diff\b|\bpatch file\b|应用补丁|统一 diff|补丁文件)/iu;
|
|
63
|
+
const TASK_INTENT_RE = /(?:\bbackground (?:task|job|command|process)\b|\btask[_-][a-z0-9]+\b|\btask log\b|后台(?:任务|命令|进程)|任务日志)/iu;
|
|
64
|
+
const SUB_AGENT_INTENT_RE = /(?:\bsub[ -]?agent\b|\bagent(?:s)?\b|\bparallel(?:ize| work| task| review)?\b|\bindependent(?:ly| review)?\b|\banother (?:worker|reviewer|agent)\b|\bdelegate\b|\b(?:run|start|launch|spawn) (?:the |a )?(?:task|child)\b|子 ?Agent|并行(?:处理|工作|任务|审查)?|独立(?:处理|审查)?|另一个(?:人|助手|Agent)|委派)/iu;
|
|
65
|
+
const WORK_ITEM_INTENT_RE = /(?:\bwork ?center\b|\bwork ?item\b|\bdurable tracking\b|\bcross[- ]turn\b|\blong[- ]running goal\b|\bacross multiple (?:turns|sessions)\b|\buntil (?:it is|it's) finished\b|工作中心|工作项|持久(?:任务|跟踪)|跨 ?turn|跨多个会话|长期任务|持续跟踪)/iu;
|
|
66
|
+
const REPL_INTENT_RE = /(?:\bjs ?repl\b|\bjavascript (?:calculation|experiment|evaluation)\b|\bcalculate\b|\bdata transform\b|JavaScript (?:计算|实验|求值)|数据转换|快速计算)/iu;
|
|
67
|
+
const NOTEBOOK_INTENT_RE = /(?:\.ipynb\b|\bjupyter\b|\bnotebook (?:cell|file)\b|Jupyter|笔记本单元格)/iu;
|
|
68
|
+
const IMAGE_GENERATION_INTENT_RE = /(?:\b(?:generate|make|design|draw) (?:me |us )?(?:an? |the )?(?:image|picture|logo|icon|illustration|graphic)\b|\bcreate (?:me |us )?(?:an? |the )?(?:illustration|image|picture|logo|icon|graphic)\b|生成(?:一张)?(?:图片|图像|标志|图标|插图)|创建(?:一张)?(?:插图|图片|图像|标志|图标)|画(?:一张)?(?:图|图片|图标))/iu;
|
|
69
|
+
const MCP_INTENT_RE = /(?:\bmcp\b|model context protocol|模型上下文协议)/iu;
|
|
70
|
+
|
|
71
|
+
function messageText(message) {
|
|
72
|
+
const content = message?.content;
|
|
73
|
+
if (typeof content === 'string') return content;
|
|
74
|
+
if (!Array.isArray(content)) return '';
|
|
75
|
+
return content
|
|
76
|
+
.filter(part => part?.type === 'text' && typeof part.text === 'string')
|
|
77
|
+
.map(part => part.text)
|
|
78
|
+
.join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build a bounded intent window from the current request and recent context.
|
|
83
|
+
* A short "continue" / "yes" turn can therefore retain a conditional tool
|
|
84
|
+
* selected by the immediately preceding discussion without making every old
|
|
85
|
+
* topic permanently activate tools.
|
|
86
|
+
*/
|
|
87
|
+
export function buildToolIntentText(prompt, messages = []) {
|
|
88
|
+
const recent = Array.isArray(messages) ? messages.slice(-6) : [];
|
|
89
|
+
const pieces = [...recent.map(messageText), typeof prompt === 'string' ? prompt : '']
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
return pieces.join('\n').slice(-12_000);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function matchedMcpTools(intentText, toolNames) {
|
|
95
|
+
const mcpTools = toolNames.filter(name => name.startsWith('mcp__'));
|
|
96
|
+
const normalized = intentText.toLowerCase();
|
|
97
|
+
const explicitMatches = mcpTools.filter(name => {
|
|
98
|
+
if (normalized.includes(name.toLowerCase())) return true;
|
|
99
|
+
const [, server = '', tool = ''] = name.split('__');
|
|
100
|
+
if (tool && normalized.includes(tool.toLowerCase())) return true;
|
|
101
|
+
return server && tool
|
|
102
|
+
&& normalized.includes(server.toLowerCase())
|
|
103
|
+
&& normalized.includes(tool.toLowerCase());
|
|
104
|
+
});
|
|
105
|
+
if (explicitMatches.length > 0) return explicitMatches;
|
|
106
|
+
return MCP_INTENT_RE.test(intentText) ? mcpTools : [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the canonical tool names exposed to one provider request.
|
|
111
|
+
*
|
|
112
|
+
* Unknown caller-registered tools remain visible for compatibility. Only
|
|
113
|
+
* Yeaft built-ins and flattened MCP tools participate in conditional hiding.
|
|
114
|
+
*
|
|
115
|
+
* @param {{
|
|
116
|
+
* toolNames: string[],
|
|
117
|
+
* prompt?: string,
|
|
118
|
+
* messages?: object[],
|
|
119
|
+
* collabToolPolicy?: string|null,
|
|
120
|
+
* activeTasks?: object[],
|
|
121
|
+
* subAgentToolsActivated?: boolean,
|
|
122
|
+
* imageGenerationConfigured?: boolean,
|
|
123
|
+
* }} opts
|
|
124
|
+
* @returns {Set<string>}
|
|
125
|
+
*/
|
|
126
|
+
export function resolveActiveToolNames({
|
|
127
|
+
toolNames = [],
|
|
128
|
+
prompt = '',
|
|
129
|
+
messages = [],
|
|
130
|
+
collabToolPolicy = null,
|
|
131
|
+
activeTasks = [],
|
|
132
|
+
subAgentToolsActivated = false,
|
|
133
|
+
imageGenerationConfigured = false,
|
|
134
|
+
} = {}) {
|
|
135
|
+
const registered = new Set(Array.isArray(toolNames) ? toolNames : []);
|
|
136
|
+
const active = new Set(ALWAYS_VISIBLE_TOOL_NAMES.filter(name => registered.has(name)));
|
|
137
|
+
const intentText = buildToolIntentText(prompt, messages);
|
|
138
|
+
const tasks = Array.isArray(activeTasks) ? activeTasks : [];
|
|
139
|
+
const hasActiveTasks = tasks.length > 0;
|
|
140
|
+
const hasSubAgentTask = tasks.some(task => task?.kind === 'sub_agent');
|
|
141
|
+
|
|
142
|
+
if (HISTORY_INTENT_RE.test(intentText)) active.add('HistorySearch');
|
|
143
|
+
if (DISK_INTENT_RE.test(intentText)) active.add('DiskUsage');
|
|
144
|
+
if (PATCH_INTENT_RE.test(intentText)) active.add('ApplyPatch');
|
|
145
|
+
|
|
146
|
+
if (hasActiveTasks || TASK_INTENT_RE.test(intentText)) {
|
|
147
|
+
for (const name of BACKGROUND_TASK_TOOL_NAMES) active.add(name);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (SUB_AGENT_INTENT_RE.test(intentText) || subAgentToolsActivated || hasSubAgentTask) {
|
|
151
|
+
active.add('SpawnAgent');
|
|
152
|
+
for (const name of SUB_AGENT_MANAGEMENT_TOOL_NAMES) active.add(name);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP) active.add('RouteForward');
|
|
156
|
+
if (WORK_ITEM_INTENT_RE.test(intentText)) active.add('CreateWorkItem');
|
|
157
|
+
if (REPL_INTENT_RE.test(intentText)) active.add('JsRepl');
|
|
158
|
+
if (NOTEBOOK_INTENT_RE.test(intentText)) active.add('NotebookEdit');
|
|
159
|
+
if (imageGenerationConfigured && IMAGE_GENERATION_INTENT_RE.test(intentText)) active.add('ImageGeneration');
|
|
160
|
+
|
|
161
|
+
for (const name of matchedMcpTools(intentText, toolNames)) active.add(name);
|
|
162
|
+
|
|
163
|
+
// Extra tools supplied by an embedding caller have no Yeaft activation
|
|
164
|
+
// policy. Preserve the historical contract and expose them by default.
|
|
165
|
+
for (const name of registered) {
|
|
166
|
+
if (ALWAYS_VISIBLE_TOOL_NAMES.includes(name)) continue;
|
|
167
|
+
if (CONDITIONAL_BUILTIN_TOOL_NAMES.has(name)) continue;
|
|
168
|
+
if (name.startsWith('mcp__')) continue;
|
|
169
|
+
active.add(name);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Never advertise deprecated schema-only compatibility shims. Direct
|
|
173
|
+
// registry execution without an active-set fence remains backward compatible.
|
|
174
|
+
active.delete('JsReplReset');
|
|
175
|
+
|
|
176
|
+
for (const name of [...active]) {
|
|
177
|
+
if (!registered.has(name)) active.delete(name);
|
|
178
|
+
}
|
|
179
|
+
return active;
|
|
180
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { defineTool } from './types.js';
|
|
2
|
+
|
|
3
|
+
export const TOOL_DISCOVERY_MAX_RESULTS = 24;
|
|
4
|
+
export const TOOL_DISCOVERY_MAX_NAME_CHARS = 256;
|
|
5
|
+
export const TOOL_DISCOVERY_MAX_DESCRIPTION_CHARS = 320;
|
|
6
|
+
export const TOOL_DISCOVERY_MAX_OUTPUT_BYTES = 12 * 1024;
|
|
7
|
+
|
|
8
|
+
const TOKEN_RE = /[\p{L}\p{N}]+/gu;
|
|
9
|
+
const STOP_WORDS = new Set([
|
|
10
|
+
'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'can', 'do', 'for', 'from',
|
|
11
|
+
'how', 'i', 'in', 'is', 'it', 'me', 'my', 'of', 'on', 'or', 'the', 'this',
|
|
12
|
+
'to', 'us', 'we', 'what', 'when', 'where', 'which', 'who', 'why', 'with', 'you',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function lowerTokens(value) {
|
|
16
|
+
const text = String(value || '');
|
|
17
|
+
if (!text) return [];
|
|
18
|
+
const expanded = text
|
|
19
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
20
|
+
.replace(/[_-]+/g, ' ');
|
|
21
|
+
return (expanded.match(TOKEN_RE) || []).map(token => token.toLocaleLowerCase());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function tokens(value) {
|
|
25
|
+
return lowerTokens(value).filter(token => token.length > 1 && !STOP_WORDS.has(token));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function truncate(value, maxChars) {
|
|
29
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
30
|
+
return text.length <= maxChars ? text : `${text.slice(0, maxChars - 1)}…`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toolSearchText(tool) {
|
|
34
|
+
return [
|
|
35
|
+
tool.name,
|
|
36
|
+
tool.description,
|
|
37
|
+
...Object.keys(tool.parameters?.properties || {}),
|
|
38
|
+
].map(value => String(value || '')).join(' ');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function scoreTool(tool, queryTokens) {
|
|
42
|
+
if (queryTokens.length === 0) return 0;
|
|
43
|
+
const name = String(tool.name || '').toLocaleLowerCase();
|
|
44
|
+
const text = toolSearchText(tool).toLocaleLowerCase();
|
|
45
|
+
const haystackTokens = new Set(tokens(text));
|
|
46
|
+
let score = 0;
|
|
47
|
+
for (const token of queryTokens) {
|
|
48
|
+
if (name.includes(token)) score += 8;
|
|
49
|
+
else if (haystackTokens.has(token)) score += 4;
|
|
50
|
+
else if (token.length >= 5 && text.includes(token)) score += 1;
|
|
51
|
+
}
|
|
52
|
+
return score;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeCursor(value) {
|
|
56
|
+
if (value == null || value === '') return 0;
|
|
57
|
+
const cursor = Number(value);
|
|
58
|
+
return Number.isInteger(cursor) && cursor >= 0 ? cursor : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validCandidate(tool) {
|
|
62
|
+
return typeof tool?.name === 'string'
|
|
63
|
+
&& tool.name.length > 0
|
|
64
|
+
&& tool.name.length <= TOOL_DISCOVERY_MAX_NAME_CHARS;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function byteLength(value) {
|
|
68
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Return one deterministic page from the complete hidden tool directory.
|
|
73
|
+
* Lexical scoring only improves ordering; it never decides reachability.
|
|
74
|
+
*/
|
|
75
|
+
export function discoverToolCapabilities({
|
|
76
|
+
query,
|
|
77
|
+
candidates = [],
|
|
78
|
+
cursor = 0,
|
|
79
|
+
maxResults = TOOL_DISCOVERY_MAX_RESULTS,
|
|
80
|
+
} = {}) {
|
|
81
|
+
const boundedMax = Math.max(1, Math.min(Number(maxResults) || TOOL_DISCOVERY_MAX_RESULTS, TOOL_DISCOVERY_MAX_RESULTS));
|
|
82
|
+
const queryTokens = [...new Set(tokens(query))];
|
|
83
|
+
let omittedInvalid = 0;
|
|
84
|
+
const scored = [];
|
|
85
|
+
for (const tool of Array.isArray(candidates) ? candidates : []) {
|
|
86
|
+
if (!validCandidate(tool)) {
|
|
87
|
+
omittedInvalid += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
scored.push({
|
|
91
|
+
tool,
|
|
92
|
+
score: scoreTool(tool, queryTokens),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
scored.sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name));
|
|
96
|
+
|
|
97
|
+
const normalizedCursor = normalizeCursor(cursor);
|
|
98
|
+
if (normalizedCursor == null || normalizedCursor > scored.length) {
|
|
99
|
+
return {
|
|
100
|
+
tools: [],
|
|
101
|
+
next_cursor: null,
|
|
102
|
+
total: scored.length,
|
|
103
|
+
omitted_invalid: omittedInvalid,
|
|
104
|
+
restart_required: true,
|
|
105
|
+
message: 'The hidden tool directory changed or the cursor is invalid. Restart discovery without a cursor.',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const start = normalizedCursor;
|
|
109
|
+
const tools = [];
|
|
110
|
+
let index = start;
|
|
111
|
+
while (index < scored.length && tools.length < boundedMax) {
|
|
112
|
+
const tool = scored[index].tool;
|
|
113
|
+
const item = {
|
|
114
|
+
name: tool.name,
|
|
115
|
+
description: truncate(tool.description, TOOL_DISCOVERY_MAX_DESCRIPTION_CHARS),
|
|
116
|
+
};
|
|
117
|
+
const hasMoreAfterItem = index + 1 < scored.length;
|
|
118
|
+
const tentative = {
|
|
119
|
+
tools: [...tools, item],
|
|
120
|
+
next_cursor: hasMoreAfterItem ? index + 1 : null,
|
|
121
|
+
total: scored.length,
|
|
122
|
+
omitted_invalid: omittedInvalid,
|
|
123
|
+
};
|
|
124
|
+
if (byteLength(tentative) > TOOL_DISCOVERY_MAX_OUTPUT_BYTES) break;
|
|
125
|
+
tools.push(item);
|
|
126
|
+
index += 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const nextCursor = index < scored.length && tools.length > 0 ? index : null;
|
|
130
|
+
const result = {
|
|
131
|
+
tools,
|
|
132
|
+
next_cursor: nextCursor,
|
|
133
|
+
total: scored.length,
|
|
134
|
+
omitted_invalid: omittedInvalid,
|
|
135
|
+
};
|
|
136
|
+
if (byteLength(result) > TOOL_DISCOVERY_MAX_OUTPUT_BYTES) {
|
|
137
|
+
return { tools: [], next_cursor: null, total: 0, omitted_invalid: omittedInvalid };
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default defineTool({
|
|
143
|
+
name: 'DiscoverTools',
|
|
144
|
+
description: {
|
|
145
|
+
en: `Discover registered tools whose full schemas are not currently active.
|
|
146
|
+
|
|
147
|
+
Use this when the visible tools do not clearly cover the user's request. Search by the user's goal or capability, not by a guessed tool name. The result is a bounded page from the complete hidden tool directory, ordered by likely relevance; lexical similarity affects ordering only and never removes capabilities. If the target is absent, continue with next_cursor until found or the directory is exhausted. If restart_required is true, discard the cursor and restart without one because the registered directory changed. Returned tool schemas become available on the next model loop.`,
|
|
148
|
+
zh: `发现已注册但当前未激活完整 schema 的工具。
|
|
149
|
+
|
|
150
|
+
当可见工具不能明确覆盖用户请求时使用。应按用户目标或能力搜索,不要猜工具名。结果是完整隐藏工具目录中的有界分页,并按可能相关性排序;词法相似度只影响顺序,绝不会让能力不可达。如果当前页没有目标,应使用 next_cursor 继续翻页直到找到或目录耗尽。如果 restart_required 为 true,说明注册目录已变化,应丢弃游标并在不带游标的情况下重新开始。返回工具的 schema 会在下一轮模型调用中可用。`,
|
|
151
|
+
},
|
|
152
|
+
parameters: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
query: {
|
|
156
|
+
type: 'string',
|
|
157
|
+
description: {
|
|
158
|
+
en: 'User goal or capability used to rank the complete hidden directory',
|
|
159
|
+
zh: '用于排序完整隐藏目录的用户目标或能力',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
cursor: {
|
|
163
|
+
type: 'number',
|
|
164
|
+
description: {
|
|
165
|
+
en: 'Pagination cursor returned by a previous discovery page',
|
|
166
|
+
zh: '上一页发现结果返回的分页游标',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
max_results: {
|
|
170
|
+
type: 'number',
|
|
171
|
+
description: {
|
|
172
|
+
en: 'Maximum directory entries to activate (default and maximum: 24)',
|
|
173
|
+
zh: '最多激活的目录项数(默认及上限为 24)',
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
required: ['query'],
|
|
178
|
+
},
|
|
179
|
+
isConcurrencySafe: () => true,
|
|
180
|
+
isReadOnly: () => true,
|
|
181
|
+
async execute(input, ctx = {}) {
|
|
182
|
+
const query = typeof input?.query === 'string' ? input.query.trim() : '';
|
|
183
|
+
if (!query) return JSON.stringify({ error: 'query is required' });
|
|
184
|
+
if (typeof ctx?.discoverTools !== 'function') {
|
|
185
|
+
return JSON.stringify({ error: 'Tool discovery is unavailable in this runtime.' });
|
|
186
|
+
}
|
|
187
|
+
const result = await ctx.discoverTools({
|
|
188
|
+
query,
|
|
189
|
+
cursor: input?.cursor,
|
|
190
|
+
maxResults: input?.max_results,
|
|
191
|
+
});
|
|
192
|
+
const output = JSON.stringify(result);
|
|
193
|
+
if (Buffer.byteLength(output, 'utf8') > TOOL_DISCOVERY_MAX_OUTPUT_BYTES) {
|
|
194
|
+
throw new Error('Tool discovery violated its bounded result budget');
|
|
195
|
+
}
|
|
196
|
+
return output;
|
|
197
|
+
},
|
|
198
|
+
});
|
package/yeaft/tools/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import askUser from './ask-user.js';
|
|
|
28
28
|
import webSearch from './web-search.js';
|
|
29
29
|
import webFetch from './web-fetch.js';
|
|
30
30
|
import historySearch from './history-search.js';
|
|
31
|
+
import discoverTools from './discover-tools.js';
|
|
31
32
|
|
|
32
33
|
// --- P0 File tools ---
|
|
33
34
|
import bash from './bash.js';
|
|
@@ -94,6 +95,7 @@ export const allTools = [
|
|
|
94
95
|
webSearch,
|
|
95
96
|
webFetch,
|
|
96
97
|
historySearch,
|
|
98
|
+
discoverTools,
|
|
97
99
|
|
|
98
100
|
// P0 File
|
|
99
101
|
bash,
|
package/yeaft/tools/registry.js
CHANGED
|
@@ -48,7 +48,7 @@ function isLocalizedTextObject(value) {
|
|
|
48
48
|
return ['en', 'zh', 'zh-CN', 'en-US', 'default'].some(key => typeof value[key] === 'string' || typeof value[key] === 'function');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function localizeVisibleText(value, language, toolName) {
|
|
51
|
+
export function localizeVisibleText(value, language, toolName) {
|
|
52
52
|
const lang = normalizeLanguage(language);
|
|
53
53
|
if (typeof value === 'function') return localizeVisibleText(value(lang), lang, toolName);
|
|
54
54
|
if (isLocalizedTextObject(value)) {
|
|
@@ -375,17 +375,25 @@ export class ToolRegistry {
|
|
|
375
375
|
|
|
376
376
|
/**
|
|
377
377
|
* Get tool definitions for the LLM adapter.
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
378
|
+
*
|
|
379
|
+
* Registration and exposure are separate: `activeToolNames` limits which
|
|
380
|
+
* canonical schemas enter this provider request, while aliases remain
|
|
381
|
+
* registered for replay compatibility. Collaboration policy is an additional
|
|
382
|
+
* structural fence (for example, RouteForward is invalid in single-VP
|
|
383
|
+
* Sessions).
|
|
384
|
+
*
|
|
381
385
|
* @param {string} [language='en']
|
|
382
|
-
* @param {{ collabToolPolicy?: string }} [opts]
|
|
386
|
+
* @param {{ collabToolPolicy?: string, activeToolNames?: Set<string>|string[] }} [opts]
|
|
383
387
|
* @returns {{ name: string, description: string, parameters: object }[]}
|
|
384
388
|
*/
|
|
385
389
|
getToolDefs(language = 'en', opts = {}) {
|
|
386
390
|
const lang = normalizeLanguage(language);
|
|
387
391
|
const collabToolPolicy = normalizeCollabToolPolicy(opts?.collabToolPolicy);
|
|
392
|
+
const activeToolNames = opts?.activeToolNames instanceof Set
|
|
393
|
+
? opts.activeToolNames
|
|
394
|
+
: (Array.isArray(opts?.activeToolNames) ? new Set(opts.activeToolNames) : null);
|
|
388
395
|
return this.getAllTools()
|
|
396
|
+
.filter(t => !activeToolNames || activeToolNames.has(t.name))
|
|
389
397
|
.filter(t => !isToolHiddenByCollabPolicy(t.name, collabToolPolicy))
|
|
390
398
|
.map(t => {
|
|
391
399
|
return {
|
|
@@ -397,16 +405,24 @@ export class ToolRegistry {
|
|
|
397
405
|
}
|
|
398
406
|
|
|
399
407
|
/**
|
|
400
|
-
* Check whether a tool may be called under the current
|
|
401
|
-
* Unknown / absent
|
|
402
|
-
*
|
|
408
|
+
* Check whether a tool may be called under the current exposure and
|
|
409
|
+
* collaboration policies. Unknown / absent policies keep the historical
|
|
410
|
+
* behavior for direct registry callers.
|
|
411
|
+
*
|
|
412
|
+
* Aliases inherit the canonical tool's activation: an old `Agent` tool call
|
|
413
|
+
* may execute only when canonical `SpawnAgent` is active for this request.
|
|
414
|
+
*
|
|
403
415
|
* @param {string} name
|
|
404
|
-
* @param {{ collabToolPolicy?: string }} [opts]
|
|
416
|
+
* @param {{ collabToolPolicy?: string, activeToolNames?: Set<string>|string[] }} [opts]
|
|
405
417
|
* @returns {boolean}
|
|
406
418
|
*/
|
|
407
419
|
isAllowed(name, opts = {}) {
|
|
408
420
|
const tool = this.#tools.get(name);
|
|
409
421
|
if (!tool) return false;
|
|
422
|
+
const activeToolNames = opts?.activeToolNames instanceof Set
|
|
423
|
+
? opts.activeToolNames
|
|
424
|
+
: (Array.isArray(opts?.activeToolNames) ? new Set(opts.activeToolNames) : null);
|
|
425
|
+
if (activeToolNames && !activeToolNames.has(tool.name)) return false;
|
|
410
426
|
return !isToolHiddenByCollabPolicy(tool.name, opts?.collabToolPolicy);
|
|
411
427
|
}
|
|
412
428
|
|
package/yeaft/tools/types.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* @property {object} [skillManager] — Skill manager
|
|
23
23
|
* @property {object} [trace] — debug trace
|
|
24
24
|
* @property {object} [config] — engine config
|
|
25
|
+
* @property {(input: {query:string, cursor?:number, maxResults?:number}) => Promise<object>|object} [discoverTools] — page through hidden registered tool capabilities and activate returned entries for the next provider loop
|
|
25
26
|
* @property {import('../tasks/manager.js').TaskManager} [taskManager] — Session task manager
|
|
26
27
|
* @property {string} [sessionId] — current Session id
|
|
27
28
|
* @property {string[]} [projectSessionIds] — same-Agent sibling Session ids in the current Project
|