@gt-fe/eap-runtime 0.0.34 → 0.0.36
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/dist/api/handlers/stream.handler.js +11 -6
- package/dist/graph/adapters/langgraph.adapter.js +75 -25
- package/dist/graph/adapters/subgraph-compiler.js +25 -0
- package/dist/graph/contracts/executable-graph.d.ts +9 -0
- package/dist/graph/nodes/llm-node.js +3 -1
- package/dist/graph/nodes/tool-node.d.ts +4 -0
- package/dist/graph/nodes/tool-node.js +132 -4
- package/dist/registry/graph-loader.js +10 -1
- package/dist/runtime/agent-runtime.js +32 -13
- package/dist/tool/index.d.ts +1 -0
- package/dist/tool/index.js +1 -0
- package/dist/tool/mcp-tool-exposure.d.ts +113 -0
- package/dist/tool/mcp-tool-exposure.js +479 -0
- package/dist/tool/system-prompt-builder.d.ts +10 -3
- package/dist/tool/system-prompt-builder.js +8 -4
- package/package.json +2 -2
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 每轮发给模型的 function schema 按工具来源决定。
|
|
3
|
+
*
|
|
4
|
+
* builtin(execution.source=builtin)以及 type 为 code / http 的工具,始终带完整参数。
|
|
5
|
+
* type=mcp 且不是 builtin 的,默认不带参数;名称和一行用途写入 system 段 mcp-tools。
|
|
6
|
+
* 点名,或没点名就调用,都不执行;名字记入本会话,下一次请求带上当前参数后再打外部系统。
|
|
7
|
+
* 没点名的那句只给模型。员工看模型正文和真正的工具结果(含飞书报错、用户拒绝),不另写未执行。
|
|
8
|
+
* 新会话不沿用。没有 sessionId 时按 taskId。EAP_DEFER_MCP_TOOLS=0 关闭,恢复为全部 schema 都下发。
|
|
9
|
+
*/
|
|
10
|
+
import { BUILTIN_TOOL_NAMES, isBuiltinToolExecution } from '@gt-fe/eap-contracts';
|
|
11
|
+
import { loadedToolsToApiTools } from './system-prompt-builder.js';
|
|
12
|
+
export const TOOL_SEARCH_NAME = 'tool_search';
|
|
13
|
+
export function deferMcpToolsEnabled(env = process.env) {
|
|
14
|
+
return env.EAP_DEFER_MCP_TOOLS !== '0';
|
|
15
|
+
}
|
|
16
|
+
export function isDeferredMcpTool(tool) {
|
|
17
|
+
return tool.manifest.type === 'mcp' && !isBuiltinToolExecution(tool.manifest.execution);
|
|
18
|
+
}
|
|
19
|
+
export function hasDeferredMcp(loadedTools) {
|
|
20
|
+
return Object.values(loadedTools ?? {}).some(isDeferredMcpTool);
|
|
21
|
+
}
|
|
22
|
+
/** 具名 toolChoice 指向的函数。auto / none / required 没有名字。 */
|
|
23
|
+
export function forcedToolName(toolChoice) {
|
|
24
|
+
if (!toolChoice || typeof toolChoice !== 'object' || Array.isArray(toolChoice))
|
|
25
|
+
return undefined;
|
|
26
|
+
const choice = toolChoice;
|
|
27
|
+
if (choice.type !== 'function')
|
|
28
|
+
return undefined;
|
|
29
|
+
const name = choice.function?.name;
|
|
30
|
+
return typeof name === 'string' && name.trim() ? name.trim() : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** 有 sessionId 用会话,否则用 taskId。空字符串表示无处可记。 */
|
|
33
|
+
export function exposureScopeId(input) {
|
|
34
|
+
const sessionId = typeof input.sessionId === 'string' ? input.sessionId.trim() : '';
|
|
35
|
+
if (sessionId)
|
|
36
|
+
return sessionId;
|
|
37
|
+
return typeof input.taskId === 'string' ? input.taskId.trim() : '';
|
|
38
|
+
}
|
|
39
|
+
export function exposedNamesForScope(names, storedScopeId, scopeId) {
|
|
40
|
+
if (!scopeId || storedScopeId !== scopeId)
|
|
41
|
+
return [];
|
|
42
|
+
if (!Array.isArray(names))
|
|
43
|
+
return [];
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
if (typeof name === 'string' && name && !out.includes(name))
|
|
47
|
+
out.push(name);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
export function mergeExposed(current, additions) {
|
|
52
|
+
const set = new Set();
|
|
53
|
+
for (const name of [...current, ...additions]) {
|
|
54
|
+
if (name)
|
|
55
|
+
set.add(name);
|
|
56
|
+
}
|
|
57
|
+
return [...set].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
58
|
+
}
|
|
59
|
+
export function resolveModelToolSurface(input) {
|
|
60
|
+
if (input.disableTools) {
|
|
61
|
+
return { apiTools: [], deferred: false };
|
|
62
|
+
}
|
|
63
|
+
const defer = deferMcpToolsEnabled(input.env);
|
|
64
|
+
const all = Object.values(input.loadedTools ?? {});
|
|
65
|
+
const deferredTools = all.filter(isDeferredMcpTool);
|
|
66
|
+
if (!defer || deferredTools.length === 0) {
|
|
67
|
+
return { apiTools: loadedToolsToApiTools(input.loadedTools), deferred: false };
|
|
68
|
+
}
|
|
69
|
+
const byName = new Map(all.map((tool) => [tool.name, tool]));
|
|
70
|
+
const ordered = [];
|
|
71
|
+
const used = new Set();
|
|
72
|
+
const push = (tool) => {
|
|
73
|
+
if (!tool || used.has(tool.name))
|
|
74
|
+
return;
|
|
75
|
+
used.add(tool.name);
|
|
76
|
+
ordered.push(tool);
|
|
77
|
+
};
|
|
78
|
+
for (const name of BUILTIN_TOOL_NAMES) {
|
|
79
|
+
const tool = byName.get(name);
|
|
80
|
+
if (tool && isBuiltinToolExecution(tool.manifest.execution))
|
|
81
|
+
push(tool);
|
|
82
|
+
}
|
|
83
|
+
const extraBuiltins = all
|
|
84
|
+
.filter((tool) => isBuiltinToolExecution(tool.manifest.execution) && !used.has(tool.name))
|
|
85
|
+
.sort(byToolName);
|
|
86
|
+
for (const tool of extraBuiltins)
|
|
87
|
+
push(tool);
|
|
88
|
+
const resident = all
|
|
89
|
+
.filter((tool) => !isDeferredMcpTool(tool) && !isBuiltinToolExecution(tool.manifest.execution))
|
|
90
|
+
.sort(byToolName);
|
|
91
|
+
for (const tool of resident)
|
|
92
|
+
push(tool);
|
|
93
|
+
const apiTools = ordered.map(toApiTool);
|
|
94
|
+
const realSearch = byName.get(TOOL_SEARCH_NAME);
|
|
95
|
+
if (!realSearch)
|
|
96
|
+
apiTools.push(toolSearchApiTool());
|
|
97
|
+
const exposed = new Set(input.exposedNames);
|
|
98
|
+
if (input.forcedName)
|
|
99
|
+
exposed.add(input.forcedName);
|
|
100
|
+
const mcpNames = [...exposed]
|
|
101
|
+
.filter((name) => {
|
|
102
|
+
const tool = byName.get(name);
|
|
103
|
+
return Boolean(tool && isDeferredMcpTool(tool));
|
|
104
|
+
})
|
|
105
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
106
|
+
for (const name of mcpNames) {
|
|
107
|
+
const tool = byName.get(name);
|
|
108
|
+
if (tool)
|
|
109
|
+
apiTools.push(toApiTool(tool));
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
apiTools,
|
|
113
|
+
mcpToolIndex: buildMcpToolIndex(input.loadedTools),
|
|
114
|
+
deferred: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** 全部推迟中的 MCP:名称 + 现有 description,不截断,不含参数 schema。 */
|
|
118
|
+
export function buildMcpToolIndex(loadedTools) {
|
|
119
|
+
const rows = Object.values(loadedTools ?? {})
|
|
120
|
+
.filter(isDeferredMcpTool)
|
|
121
|
+
.sort(byToolName)
|
|
122
|
+
.map((tool) => {
|
|
123
|
+
const desc = oneLine(tool.manifest.description);
|
|
124
|
+
return `- ${tool.name}${desc ? `:${desc}` : ''}`;
|
|
125
|
+
});
|
|
126
|
+
if (rows.length === 0)
|
|
127
|
+
return undefined;
|
|
128
|
+
return [
|
|
129
|
+
'# 可用工具',
|
|
130
|
+
'下面这些 MCP 工具本轮没有参数说明。先调用 tool_search,把 names 设为工具名。下一次模型调用会带上完整参数,然后再调用。没点名的直接调用不会执行。',
|
|
131
|
+
rows.join('\n'),
|
|
132
|
+
].join('\n');
|
|
133
|
+
}
|
|
134
|
+
export function toolSearchApiTool() {
|
|
135
|
+
return {
|
|
136
|
+
type: 'function',
|
|
137
|
+
function: {
|
|
138
|
+
name: TOOL_SEARCH_NAME,
|
|
139
|
+
description: '按名称准备 MCP 工具的完整参数。names:下一次模型调用带上这些工具的当前参数,这一次不执行。query:只返回命中的名称,不带参数,也不加入下一次调用。两个都可以不填,但都不填时不会列出全部工具。',
|
|
140
|
+
parameters: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
names: {
|
|
144
|
+
description: '要加载参数的 MCP 工具名。可以是字符串、逗号分隔的字符串,或字符串数组。',
|
|
145
|
+
anyOf: [
|
|
146
|
+
{ type: 'array', items: { type: 'string' } },
|
|
147
|
+
{ type: 'string' },
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
query: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
description: '不足两个字忽略。名字和搜索词一方包含另一方,或整段搜索词原样出现在用途里时命中。只返回名称,不带参数。',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const NAME_MISS = '已登记的工具里没有这个名字。MCP 的名字以「可用工具」目录为准,不要告诉用户平台没有这类能力。';
|
|
160
|
+
const QUERY_MISS = '没有命中已登记的 MCP。名字见「可用工具」目录,用 names 点名。';
|
|
161
|
+
export function unloadedCallToolContent(name) {
|
|
162
|
+
return `未调用 ${name}:参数尚未交给模型,外部系统未执行。请按当前参数重新调用,不要告诉用户已经执行。`;
|
|
163
|
+
}
|
|
164
|
+
export function isUnloadedDeferredCall(tool, sentToolNames) {
|
|
165
|
+
if (!Array.isArray(sentToolNames) || !tool || !isDeferredMcpTool(tool))
|
|
166
|
+
return false;
|
|
167
|
+
return !sentToolNames.includes(tool.name);
|
|
168
|
+
}
|
|
169
|
+
export function runToolSearch(args, loadedTools, alreadyInTools) {
|
|
170
|
+
const names = parseNames(args?.names);
|
|
171
|
+
const rawQuery = typeof args?.query === 'string' ? args.query.trim() : '';
|
|
172
|
+
const query = rawQuery.length >= 2 ? rawQuery : '';
|
|
173
|
+
const tools = Object.values(loadedTools ?? {});
|
|
174
|
+
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
175
|
+
const resident = new Set(alreadyInTools ?? []);
|
|
176
|
+
if (names.length === 0 && !query) {
|
|
177
|
+
return {
|
|
178
|
+
content: '请传入 names(要在下一次模型调用加载参数的 MCP 工具名)或 query(按名称或整段用途匹配)。这次没有列出全部工具。',
|
|
179
|
+
expose: [],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const lines = [];
|
|
183
|
+
const expose = [];
|
|
184
|
+
for (const name of names) {
|
|
185
|
+
const tool = byName.get(name);
|
|
186
|
+
if (!tool || !isDeferredMcpTool(tool)) {
|
|
187
|
+
if (tool) {
|
|
188
|
+
lines.push(`${name}:已在本轮 tools 里,可以直接调用。`);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
lines.push(`${name}:${NAME_MISS}`);
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (resident.has(name)) {
|
|
196
|
+
lines.push(`${name}:已在本轮 tools 里,可以直接调用。`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
expose.push(name);
|
|
200
|
+
lines.push(`${name}:下一次模型调用会带上完整参数。这一次不执行。`);
|
|
201
|
+
}
|
|
202
|
+
if (query) {
|
|
203
|
+
const needle = query.toLowerCase();
|
|
204
|
+
const matches = tools
|
|
205
|
+
.filter(isDeferredMcpTool)
|
|
206
|
+
.filter((tool) => queryMatches(tool, needle))
|
|
207
|
+
.sort(byToolName);
|
|
208
|
+
if (matches.length === 0) {
|
|
209
|
+
lines.push(QUERY_MISS);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
lines.push(matches.map((tool) => tool.name).join('、'));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { content: lines.join('\n'), expose };
|
|
216
|
+
}
|
|
217
|
+
export function partitionToolSearchCalls(calls, loadedTools, env) {
|
|
218
|
+
const deferActive = deferMcpToolsEnabled(env) && hasDeferredMcp(loadedTools);
|
|
219
|
+
const intercept = deferActive && !loadedTools?.[TOOL_SEARCH_NAME];
|
|
220
|
+
if (!intercept) {
|
|
221
|
+
return { deferActive, intercept: false, search: [], business: calls };
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
deferActive,
|
|
225
|
+
intercept: true,
|
|
226
|
+
search: calls.filter((call) => call.name === TOOL_SEARCH_NAME),
|
|
227
|
+
business: calls.filter((call) => call.name !== TOOL_SEARCH_NAME),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** tool_search 点名,以及本轮调用过的 MCP,记入本会话。直接调用这一次不执行。 */
|
|
231
|
+
export function mcpNamesToExpose(loadedTools, searchExpose, businessNames) {
|
|
232
|
+
const additions = [...searchExpose];
|
|
233
|
+
for (const name of businessNames) {
|
|
234
|
+
const tool = loadedTools?.[name];
|
|
235
|
+
if (tool && isDeferredMcpTool(tool))
|
|
236
|
+
additions.push(name);
|
|
237
|
+
}
|
|
238
|
+
return additions;
|
|
239
|
+
}
|
|
240
|
+
const NOT_CALLED_LINE = /^未调用 (.+?):/;
|
|
241
|
+
const USER_REJECTED_LINE = /用户拒绝执行工具 (\S+)/;
|
|
242
|
+
/** 发给模型的历史。不改会话存档。只处理这次 tools 里没有的名字。 */
|
|
243
|
+
export function projectHistoryForModelTools(messages, toolNames) {
|
|
244
|
+
const dropped = new Map();
|
|
245
|
+
for (const message of messages) {
|
|
246
|
+
if (message.role !== 'assistant' || !message.toolCalls?.length)
|
|
247
|
+
continue;
|
|
248
|
+
for (const call of message.toolCalls) {
|
|
249
|
+
if (toolNames.has(call.name))
|
|
250
|
+
continue;
|
|
251
|
+
const toolMessage = messages.find((item) => item.role === 'tool' && item.toolCallId === call.id);
|
|
252
|
+
const body = toolMessage?.content ?? '';
|
|
253
|
+
dropped.set(call.id, body.startsWith('未调用 ') || !toolMessage
|
|
254
|
+
? `未调用 ${call.name}:参数尚未交给模型,外部系统未执行。`
|
|
255
|
+
: body);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (dropped.size === 0)
|
|
259
|
+
return messages;
|
|
260
|
+
const out = [];
|
|
261
|
+
for (const message of messages) {
|
|
262
|
+
if (message.role === 'tool' && message.toolCallId && dropped.has(message.toolCallId))
|
|
263
|
+
continue;
|
|
264
|
+
if (message.role !== 'assistant' || !message.toolCalls?.length) {
|
|
265
|
+
out.push(message);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const kept = message.toolCalls.filter((call) => !dropped.has(call.id));
|
|
269
|
+
const notes = message.toolCalls
|
|
270
|
+
.filter((call) => dropped.has(call.id))
|
|
271
|
+
.map((call) => dropped.get(call.id) ?? '')
|
|
272
|
+
.filter((note) => note.length > 0);
|
|
273
|
+
// 说明放在仍保留的 function call 前面,避免插进 call 和 tool 结果之间。
|
|
274
|
+
for (const note of notes) {
|
|
275
|
+
out.push({ role: 'assistant', content: note });
|
|
276
|
+
}
|
|
277
|
+
if (kept.length > 0 || message.content.trim()) {
|
|
278
|
+
const next = { ...message };
|
|
279
|
+
if (kept.length > 0)
|
|
280
|
+
next.toolCalls = kept;
|
|
281
|
+
else
|
|
282
|
+
delete next.toolCalls;
|
|
283
|
+
out.push(next);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* 员工可见正文。未点名、只报了名,都不改模型的话。
|
|
290
|
+
* 用户拒绝仍补一行。飞书自己的成功或错误不改。
|
|
291
|
+
*/
|
|
292
|
+
export function correctEmployeeVisibleReply(visible, tools) {
|
|
293
|
+
const refused = new Map();
|
|
294
|
+
const executedNames = new Set();
|
|
295
|
+
for (const tool of tools) {
|
|
296
|
+
const name = tool.toolName || tool.name || '';
|
|
297
|
+
const content = typeof tool.content === 'string' ? tool.content : '';
|
|
298
|
+
if (NOT_CALLED_LINE.test(content) || name === TOOL_SEARCH_NAME)
|
|
299
|
+
continue;
|
|
300
|
+
const rejected = USER_REJECTED_LINE.exec(content);
|
|
301
|
+
if (rejected) {
|
|
302
|
+
refused.set(rejected[1], `已拒绝执行 ${rejected[1]},外部系统未调用。`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (name)
|
|
306
|
+
executedNames.add(name);
|
|
307
|
+
}
|
|
308
|
+
for (const name of executedNames)
|
|
309
|
+
refused.delete(name);
|
|
310
|
+
if (refused.size === 0)
|
|
311
|
+
return visible;
|
|
312
|
+
const extra = [...refused.values()].filter((line) => !visible.includes(line));
|
|
313
|
+
if (extra.length === 0)
|
|
314
|
+
return visible;
|
|
315
|
+
return visible.trim() ? `${visible.trim()}\n${extra.join('\n')}` : extra.join('\n');
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* 还有未点名的 MCP 时,这段模型正文先攒着,等这条消息落定再决定发不发给员工。
|
|
319
|
+
* 全部已经在本次 tools 里,或本次没有 tools,仍逐 token 推。
|
|
320
|
+
*/
|
|
321
|
+
export function holdEmployeeTextForUnnamedMcp(loadedTools, sentToolNames) {
|
|
322
|
+
if (!deferMcpToolsEnabled() || !loadedTools || sentToolNames.length === 0)
|
|
323
|
+
return false;
|
|
324
|
+
const sent = new Set(sentToolNames);
|
|
325
|
+
return Object.values(loadedTools).some((tool) => isDeferredMcpTool(tool) && !sent.has(tool.name));
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* 这条模型消息还不能当员工结论:带着没点名的调用,或这一次只点了名。
|
|
329
|
+
* 没有工具调用时不丢,交给回合结束时的闸门。
|
|
330
|
+
*/
|
|
331
|
+
export function shouldDropUnsettledEmployeeText(calls, loadedTools, sentToolNames) {
|
|
332
|
+
if (!calls?.length)
|
|
333
|
+
return false;
|
|
334
|
+
if (calls.some((call) => isUnloadedDeferredCall(loadedTools?.[call.name], sentToolNames)))
|
|
335
|
+
return true;
|
|
336
|
+
return calls.every((call) => call.name === TOOL_SEARCH_NAME);
|
|
337
|
+
}
|
|
338
|
+
function employeePayload(payload) {
|
|
339
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
340
|
+
return payload;
|
|
341
|
+
}
|
|
342
|
+
return {};
|
|
343
|
+
}
|
|
344
|
+
function employeeToolText(payload) {
|
|
345
|
+
if (typeof payload.content === 'string' && payload.content)
|
|
346
|
+
return payload.content;
|
|
347
|
+
if (typeof payload.error === 'string')
|
|
348
|
+
return payload.error;
|
|
349
|
+
return '';
|
|
350
|
+
}
|
|
351
|
+
function isHiddenEmployeeToolEvent(eventType, payload) {
|
|
352
|
+
if (eventType !== 'tool.called' &&
|
|
353
|
+
eventType !== 'tool.progress' &&
|
|
354
|
+
eventType !== 'tool.result' &&
|
|
355
|
+
eventType !== 'tool.error') {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
if (payload.modelOnly === true)
|
|
359
|
+
return true;
|
|
360
|
+
if (payload.toolName === TOOL_SEARCH_NAME)
|
|
361
|
+
return true;
|
|
362
|
+
return employeeToolText(payload).startsWith('未调用 ');
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* 没点名的工具事件和 tool_search 不进员工 SSE。
|
|
366
|
+
* 判定:`modelOnly`,或工具名是 `tool_search`,或正文以 `未调用 ` 开头。
|
|
367
|
+
*/
|
|
368
|
+
export function takeEmployeeVisibleEvents(eventType, payload) {
|
|
369
|
+
const body = employeePayload(payload);
|
|
370
|
+
if (isHiddenEmployeeToolEvent(eventType, body))
|
|
371
|
+
return [];
|
|
372
|
+
return [{ type: eventType, payload: body }];
|
|
373
|
+
}
|
|
374
|
+
export function employeeTurnClose(raw, tools) {
|
|
375
|
+
const visible = correctEmployeeVisibleReply(raw, tools);
|
|
376
|
+
const base = raw.trim();
|
|
377
|
+
const next = visible.trim();
|
|
378
|
+
if (next === base)
|
|
379
|
+
return { visible, tailChunk: '' };
|
|
380
|
+
if (!base)
|
|
381
|
+
return { visible, tailChunk: next };
|
|
382
|
+
if (next.startsWith(base)) {
|
|
383
|
+
const rest = next.slice(base.length).replace(/^\n+/, '');
|
|
384
|
+
return { visible, tailChunk: rest ? `\n${rest}` : '' };
|
|
385
|
+
}
|
|
386
|
+
return { visible, tailChunk: `\n${next}` };
|
|
387
|
+
}
|
|
388
|
+
/** 回合结束时要补发的 response.chunk。final 仍是 injected。 */
|
|
389
|
+
export function employeeStreamClose(input) {
|
|
390
|
+
const close = employeeTurnClose(input.raw, input.tools);
|
|
391
|
+
const chunks = [];
|
|
392
|
+
if (close.tailChunk)
|
|
393
|
+
chunks.push(close.tailChunk);
|
|
394
|
+
if (input.suffix)
|
|
395
|
+
chunks.push(input.suffix);
|
|
396
|
+
return {
|
|
397
|
+
chunks,
|
|
398
|
+
persistCorrected: close.tailChunk.length > 0 || Boolean(input.suffix),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function messagesSinceLastUser(messages) {
|
|
402
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
403
|
+
const message = messages[i];
|
|
404
|
+
if (message && typeof message === 'object' && message.role === 'user') {
|
|
405
|
+
return messages.slice(i + 1);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return messages;
|
|
409
|
+
}
|
|
410
|
+
export function toolFactsSinceLastUser(messages) {
|
|
411
|
+
const facts = [];
|
|
412
|
+
for (const message of messagesSinceLastUser(messages)) {
|
|
413
|
+
if (!message || typeof message !== 'object')
|
|
414
|
+
continue;
|
|
415
|
+
const record = message;
|
|
416
|
+
if (record.role !== 'tool')
|
|
417
|
+
continue;
|
|
418
|
+
facts.push({
|
|
419
|
+
toolName: typeof record.toolName === 'string' ? record.toolName : undefined,
|
|
420
|
+
content: record.content,
|
|
421
|
+
success: typeof record.success === 'boolean' ? record.success : undefined,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return facts;
|
|
425
|
+
}
|
|
426
|
+
/** 本轮最后一条助手正文。图若停在 tool 节点,不用工具正文当员工结论。 */
|
|
427
|
+
export function lastAssistantTextSinceLastUser(messages) {
|
|
428
|
+
let content = '';
|
|
429
|
+
for (const message of messagesSinceLastUser(messages)) {
|
|
430
|
+
if (!message || typeof message !== 'object')
|
|
431
|
+
continue;
|
|
432
|
+
const record = message;
|
|
433
|
+
if (record.role !== 'assistant')
|
|
434
|
+
continue;
|
|
435
|
+
content = typeof record.content === 'string' ? record.content : '';
|
|
436
|
+
}
|
|
437
|
+
return content;
|
|
438
|
+
}
|
|
439
|
+
function toApiTool(tool) {
|
|
440
|
+
return {
|
|
441
|
+
type: 'function',
|
|
442
|
+
function: {
|
|
443
|
+
name: tool.name,
|
|
444
|
+
description: tool.manifest.description,
|
|
445
|
+
parameters: tool.inputSchema ?? { type: 'object', properties: {} },
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function byToolName(a, b) {
|
|
450
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
451
|
+
}
|
|
452
|
+
function oneLine(value) {
|
|
453
|
+
return (value ?? '').replace(/\s+/g, ' ').trim();
|
|
454
|
+
}
|
|
455
|
+
function queryMatches(tool, needle) {
|
|
456
|
+
const name = tool.name.toLowerCase();
|
|
457
|
+
const description = (tool.manifest.description ?? '').toLowerCase();
|
|
458
|
+
return name.includes(needle) || needle.includes(name) || description.includes(needle);
|
|
459
|
+
}
|
|
460
|
+
function parseNames(value) {
|
|
461
|
+
const raw = [];
|
|
462
|
+
if (typeof value === 'string')
|
|
463
|
+
raw.push(value);
|
|
464
|
+
else if (Array.isArray(value)) {
|
|
465
|
+
for (const item of value) {
|
|
466
|
+
if (typeof item === 'string')
|
|
467
|
+
raw.push(item);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const out = [];
|
|
471
|
+
for (const item of raw) {
|
|
472
|
+
for (const part of item.split(',')) {
|
|
473
|
+
const name = part.trim();
|
|
474
|
+
if (name && !out.includes(name))
|
|
475
|
+
out.push(name);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 基线只走 GraphState.agentSystemPrompt,不写进节点 config。无图时节点无增量。
|
|
17
17
|
* silent 节点不带基线。技能正文不灌 system。
|
|
18
18
|
*
|
|
19
|
-
*
|
|
19
|
+
* builtin、code、http 的参数走 API tools。推迟下发的 MCP 只把名称和用途写入 system 段 mcp-tools。
|
|
20
20
|
*
|
|
21
21
|
* @see docs/status/module-status-and-backlog.md RT-5
|
|
22
22
|
*/
|
|
@@ -97,6 +97,11 @@ export interface PromptBuildContext {
|
|
|
97
97
|
}>;
|
|
98
98
|
/** 已装配 Skill 短目录;单独成段,不并进 Agent 基线 / 节点提示词 */
|
|
99
99
|
skillCatalog?: string;
|
|
100
|
+
/**
|
|
101
|
+
* 推迟下发参数的 MCP 工具:名称和现有 description。
|
|
102
|
+
* 空则省略。不含 JSON Schema。
|
|
103
|
+
*/
|
|
104
|
+
mcpToolIndex?: string;
|
|
100
105
|
}
|
|
101
106
|
export interface PromptSection {
|
|
102
107
|
id: string;
|
|
@@ -127,7 +132,8 @@ export declare function resolveLlmPromptInputs(input: {
|
|
|
127
132
|
* SystemPromptBuilder — 系统提示词拼接
|
|
128
133
|
*
|
|
129
134
|
* 具名段拼接(无模板引擎)。顺序固定,段标题用于排查。
|
|
130
|
-
*
|
|
135
|
+
* builtin、code、http 的参数走 API tools。推迟下发的 MCP 在段 mcp-tools 写名称和用途。
|
|
136
|
+
* 技能短目录进 system(技能不是 function)。
|
|
131
137
|
* Agent 基线与节点提示词整段透传,不做 {{ }} 替换。
|
|
132
138
|
*/
|
|
133
139
|
export declare class SystemPromptBuilder {
|
|
@@ -161,7 +167,8 @@ export declare function buildSkillCatalog(defs: LoadedSkillMap | undefined): str
|
|
|
161
167
|
/**
|
|
162
168
|
* 将平台的 LoadedToolMap 转为 OpenAI API 的 tools 参数
|
|
163
169
|
*
|
|
164
|
-
* 工具通过 API 的 tools 参数传入 LLM
|
|
170
|
+
* 工具通过 API 的 tools 参数传入 LLM。
|
|
171
|
+
* 主图是否把 MCP 放进本轮,见 resolveModelToolSurface。本函数仍按传入的映射原样转换。
|
|
165
172
|
* 可按需传入 skillTools 过滤子集(只暴露当前 Skill 需要的工具)。
|
|
166
173
|
*
|
|
167
174
|
* @param loadedTools - 已装配的工具映射
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 基线只走 GraphState.agentSystemPrompt,不写进节点 config。无图时节点无增量。
|
|
17
17
|
* silent 节点不带基线。技能正文不灌 system。
|
|
18
18
|
*
|
|
19
|
-
*
|
|
19
|
+
* builtin、code、http 的参数走 API tools。推迟下发的 MCP 只把名称和用途写入 system 段 mcp-tools。
|
|
20
20
|
*
|
|
21
21
|
* @see docs/status/module-status-and-backlog.md RT-5
|
|
22
22
|
*/
|
|
@@ -31,6 +31,7 @@ const PLATFORM_RULES = `你是运行在企业智能体平台(EAP)上的 AI
|
|
|
31
31
|
- **调用前说明:** 每次调用前用一句短话告诉用户你在做什么,例如「我来查上个月的销售数据」。
|
|
32
32
|
- **并行:** 无数据依赖的调用必须同一轮同时发起;只有后续依赖前序结果时才分轮。严禁无依赖却串行。
|
|
33
33
|
- **严禁**调用未提供的工具。
|
|
34
|
+
- MCP 工具先用 tool_search 的 names 点名。下一次模型调用带上参数之后再调用。没点名的直接调用不会执行,不要告诉用户已经查到或已经办成。
|
|
34
35
|
- **参数只能来自:**(1)用户明确提供(2)对话里已确认的上下文(3)前序工具返回。缺必填参数就问,禁止猜测。编码、ID、路径、URL 绝对不可凭印象填写。
|
|
35
36
|
- 同一组语义等价参数已经成功返回时,直接用该结果,不要原样重打。
|
|
36
37
|
- 失败时读错误信息再改参数。返回「无权限」则告知用户,不要反复重试。
|
|
@@ -50,7 +51,7 @@ const PLATFORM_RULES = `你是运行在企业智能体平台(EAP)上的 AI
|
|
|
50
51
|
- 数据为空如实说,不编造。一次只问一个澄清问题。
|
|
51
52
|
- 用户说「以上 / 刚才 / 好的 / 需要」且上下文够用时,直接执行,不要再问一遍。
|
|
52
53
|
- 用户问「你可以干什么 / 有什么能力」:用人话概述能帮他做哪类事。点到已有工具和技能的用途即可,不要逐项念内部名称;连通性占位(如 echo-tool)可以不提。
|
|
53
|
-
- 用户问「有哪些工具 /
|
|
54
|
+
- 用户问「有哪些工具 / 有哪些技能」:已在 API tools 中的工具按该清单列出;MCP 工具按「可用工具」目录列出名称与用途;技能按「可用技能」列出。
|
|
54
55
|
- 任务对上某条技能的 description 时直接用(先 read_file 读其 SKILL.md),用户不必先问清单或点名技能。
|
|
55
56
|
|
|
56
57
|
## 安全
|
|
@@ -119,7 +120,8 @@ export function resolveLlmPromptInputs(input) {
|
|
|
119
120
|
* SystemPromptBuilder — 系统提示词拼接
|
|
120
121
|
*
|
|
121
122
|
* 具名段拼接(无模板引擎)。顺序固定,段标题用于排查。
|
|
122
|
-
*
|
|
123
|
+
* builtin、code、http 的参数走 API tools。推迟下发的 MCP 在段 mcp-tools 写名称和用途。
|
|
124
|
+
* 技能短目录进 system(技能不是 function)。
|
|
123
125
|
* Agent 基线与节点提示词整段透传,不做 {{ }} 替换。
|
|
124
126
|
*/
|
|
125
127
|
export class SystemPromptBuilder {
|
|
@@ -141,6 +143,7 @@ export class SystemPromptBuilder {
|
|
|
141
143
|
add('user', '当前用户', `## 当前用户\n用户标识:${context.userId.trim()}。用户说「我 / 我的」时使用此标识,不要向用户再要一遍。`);
|
|
142
144
|
}
|
|
143
145
|
add('jail', '文件与命令权限', this.buildJail(context));
|
|
146
|
+
add('mcp-tools', '可用工具', context.mcpToolIndex);
|
|
144
147
|
add('skills-catalog', '可用技能', context.skillCatalog);
|
|
145
148
|
add('agent', 'Agent 基线', context.agentPrompt);
|
|
146
149
|
add('node', '节点提示词', context.nodePrompt);
|
|
@@ -279,7 +282,8 @@ export function buildSkillCatalog(defs) {
|
|
|
279
282
|
/**
|
|
280
283
|
* 将平台的 LoadedToolMap 转为 OpenAI API 的 tools 参数
|
|
281
284
|
*
|
|
282
|
-
* 工具通过 API 的 tools 参数传入 LLM
|
|
285
|
+
* 工具通过 API 的 tools 参数传入 LLM。
|
|
286
|
+
* 主图是否把 MCP 放进本轮,见 resolveModelToolSurface。本函数仍按传入的映射原样转换。
|
|
283
287
|
* 可按需传入 skillTools 过滤子集(只暴露当前 Skill 需要的工具)。
|
|
284
288
|
*
|
|
285
289
|
* @param loadedTools - 已装配的工具映射
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gt-fe/eap-runtime",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.36",
|
|
4
4
|
"description": "Enterprise Agent Platform — Runtime核心:Graph执行引擎、Task生命周期、Session管理、Checkpoint持久化",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"ioredis": "^5.4.0",
|
|
21
21
|
"pg": "^8.13.0",
|
|
22
22
|
"yaml": "^2.6.1",
|
|
23
|
-
"@gt-fe/eap-contracts": "0.0.
|
|
23
|
+
"@gt-fe/eap-contracts": "0.0.18"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"registry": "https://registry.npmjs.org/",
|