♻️ refactor(context-engine): drop ____builtin suffix from tool names (#14289)

♻️ refactor(context-engine): drop ____builtin suffix from tool names

Builtin tools now generate two-segment names like documents____upsertDocumentByFilename instead of documents____upsertDocumentByFilename____builtin. The "default" plugin type was already suffix-less, and "default" is no longer in active use, so collapsing builtin into the same shape removes redundant LLM-facing tokens. resolve() falls back to type 'builtin' for two-segment names and still parses legacy three-segment ____builtin names from message history.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arvin Xu
2026-04-29 11:25:24 +08:00
committed by GitHub
parent 2965cbc83a
commit fbe8ab3891
22 changed files with 144 additions and 113 deletions
@@ -117,7 +117,7 @@ it('should handle tool calls', async () => {
toolCalls: [
{
id: 'call_123',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
arguments: JSON.stringify({ query: 'weather' }),
},
],
@@ -54,7 +54,7 @@ export const formatWebOnboardingStateMessage = (state: OnboardingStateContext) =
const phaseGuidance = PHASE_GUIDANCE[state.phase] || '';
const parts: string[] = [
phaseGuidance,
'Questioning rule: prefer the `lobe-user-interaction____askUserQuestion____builtin` tool call for structured collection or explicit UI input. For natural exploratory questions, plain text is allowed.',
'Questioning rule: prefer the `lobe-user-interaction____askUserQuestion` tool call for structured collection or explicit UI input. For natural exploratory questions, plain text is allowed.',
];
if (state.remainingDiscoveryExchanges !== undefined && state.remainingDiscoveryExchanges > 0) {
@@ -6,7 +6,7 @@ Turn protocol:
2. Follow the phase indicated in the injected context. Do not advance the flow out of order. Exception: if the user clearly signals they want to leave (busy, disengaging, says goodbye), skip directly to a brief wrap-up and call finishOnboarding regardless of the current phase.
3. **Each turn, the system appends a \`<next_actions>\` directive after the user's message. You MUST follow the tool call instructions in \`<next_actions>\` — they tell you exactly which persistence tools to call based on the current phase and missing data. Treat \`<next_actions>\` as mandatory operational instructions, not suggestions.**
4. Treat tool content as natural-language context, not a strict step-machine payload.
5. Prefer the \`lobe-user-interaction____askUserQuestion____builtin\` tool call for structured collection, explicit choices, or UI-mediated input. For natural exploratory conversation, direct plain-text questions are allowed and often preferable.
5. Prefer the \`lobe-user-interaction____askUserQuestion\` tool call for structured collection, explicit choices, or UI-mediated input. For natural exploratory conversation, direct plain-text questions are allowed and often preferable.
6. Never claim something was saved, updated, created, or completed unless the corresponding tool call succeeded. If a tool call fails, recover from that result only.
7. Never finish onboarding before the summary is shown and lightly confirmed, unless the user clearly signals they want to leave.
8. **CRITICAL: You MUST call persistence tools (saveUserQuestion, writeDocument, updateDocument) throughout the entire conversation, not just at the beginning. Every time you learn new information about the user, persist it promptly. When the user signals completion (e.g., "好了", "谢谢", "行", "Done"), you MUST call finishOnboarding — this is a hard requirement that overrides all other rules.**
@@ -591,7 +591,7 @@ Document content here.
{
function: {
arguments: '{}',
name: 'lobe-page-agent____modifyNodes____builtin',
name: 'lobe-page-agent____modifyNodes',
},
id: 'call_1',
type: 'function',
@@ -631,8 +631,7 @@ Document content here.
expect(
result.messages.some(
(m) =>
m.role === 'assistant' &&
JSON.stringify(m).includes('lobe-page-agent____modifyNodes____builtin'),
m.role === 'assistant' && JSON.stringify(m).includes('lobe-page-agent____modifyNodes'),
),
).toBe(false);
expect(result.metadata.disabledToolCallFilter).toEqual({
@@ -25,11 +25,12 @@ export class ToolNameResolver {
* Generate tool calling name
* @param identifier - Plugin identifier
* @param name - API name
* @param type - Plugin type (default: 'default')
* @param type - Plugin type (default: 'builtin')
* @returns Generated tool name (max 64 characters)
*/
generate(identifier: string, name: string, type: string = 'default'): string {
const pluginType = type && type !== 'default' ? `${PLUGIN_SCHEMA_SEPARATOR}${type}` : '';
generate(identifier: string, name: string, type: string = 'builtin'): string {
const pluginType =
type && type !== 'builtin' && type !== 'default' ? `${PLUGIN_SCHEMA_SEPARATOR}${type}` : '';
// Step 1: Try normal format
let apiName = identifier + PLUGIN_SCHEMA_SEPARATOR + name + pluginType;
@@ -86,7 +87,7 @@ export class ToolNameResolver {
id: toolCall.id,
identifier,
thoughtSignature: toolCall.thoughtSignature,
type: (type ?? manifests[identifier]?.type ?? 'default') as any,
type: (type ?? manifests[identifier]?.type ?? 'builtin') as any,
};
// Step 2: Resolve hashed apiName if needed
@@ -10,17 +10,22 @@ describe('ToolNameResolver', () => {
expect(result).toBe('test-plugin____myAction');
});
it('should generate tool name with type suffix', () => {
const result = resolver.generate('test-plugin', 'myAction', 'builtin');
expect(result).toBe('test-plugin____myAction____builtin');
it('should generate tool name with non-builtin type suffix', () => {
const result = resolver.generate('test-plugin', 'myAction', 'standalone');
expect(result).toBe('test-plugin____myAction____standalone');
});
it('should handle default type', () => {
it('should treat builtin type as default and skip the suffix', () => {
const result = resolver.generate('test-plugin', 'myAction', 'builtin');
expect(result).toBe('test-plugin____myAction');
});
it('should treat legacy default type the same as builtin', () => {
const result = resolver.generate('test-plugin', 'myAction', 'default');
expect(result).toBe('test-plugin____myAction');
});
it('should handle undefined type as default', () => {
it('should handle undefined type as builtin', () => {
const result = resolver.generate('test-plugin', 'myAction');
expect(result).toBe('test-plugin____myAction');
});
@@ -35,14 +40,14 @@ describe('ToolNameResolver', () => {
const result = resolver.generate(identifier, longActionName, 'builtin');
// The result should be shorter than the original would have been
const originalLength = `${identifier}____${longActionName}____builtin`.length;
const originalLength = `${identifier}____${longActionName}`.length;
expect(result.length).toBeLessThan(originalLength);
// Should contain the identifier, MD5HASH prefix, and type
// Builtin tools have no type suffix; identifier and MD5HASH prefix remain
expect(result).toContain(identifier);
expect(result).toContain('MD5HASH_');
expect(result).toContain('____builtin');
expect(result).toMatch(/^my-plugin_{4}MD5HASH_[\da-f]+_{4}builtin$/);
expect(result).not.toContain('____builtin');
expect(result).toMatch(/^my-plugin_{4}MD5HASH_[\da-f]+$/);
});
it('should handle identifier that is itself long', () => {
@@ -89,12 +94,12 @@ describe('ToolNameResolver', () => {
describe('generate - special characters and edge cases', () => {
it('should handle identifiers with special characters', () => {
const result = resolver.generate('my-plugin_v2', 'action-name', 'builtin');
expect(result).toBe('my-plugin_v2____action-name____builtin');
expect(result).toBe('my-plugin_v2____action-name');
});
it('should handle empty action name', () => {
const result = resolver.generate('plugin', '', 'builtin');
expect(result).toBe('plugin________builtin');
expect(result).toBe('plugin____');
});
it('should handle numeric identifiers and action names', () => {
@@ -118,7 +123,8 @@ describe('ToolNameResolver', () => {
describe('generate - hash consistency', () => {
it('should generate consistent hash for same long action name', () => {
const identifier = 'plugin';
const longActionName = 'very-long-action-name-that-will-also-cause-overflow';
const longActionName =
'very-long-action-name-that-will-also-cause-overflow-with-extra-padding';
const result1 = resolver.generate(identifier, longActionName, 'builtin');
const result2 = resolver.generate(identifier, longActionName, 'builtin');
@@ -129,8 +135,8 @@ describe('ToolNameResolver', () => {
it('should generate different hashes for different long action names', () => {
const identifier = 'plugin';
const longActionName1 = 'very-long-action-name-that-will-also-cause-overflow-1';
const longActionName2 = 'very-long-action-name-that-will-also-cause-overflow-2';
const longActionName1 = 'very-long-action-name-that-will-also-cause-overflow-with-padding-1';
const longActionName2 = 'very-long-action-name-that-will-also-cause-overflow-with-padding-2';
const result1 = resolver.generate(identifier, longActionName1, 'builtin');
const result2 = resolver.generate(identifier, longActionName2, 'builtin');
@@ -144,15 +150,15 @@ describe('ToolNameResolver', () => {
describe('generate - real-world examples', () => {
it('should handle builtin tools correctly', () => {
const result = resolver.generate('lobe-image-designer', 'text2image', 'builtin');
expect(result).toBe('lobe-image-designer____text2image____builtin');
expect(result).toBe('lobe-image-designer____text2image');
});
it('should handle web browsing tools correctly', () => {
const result = resolver.generate('lobe-web-browsing', 'search', 'builtin');
expect(result).toBe('lobe-web-browsing____search____builtin');
expect(result).toBe('lobe-web-browsing____search');
const result2 = resolver.generate('lobe-web-browsing', 'crawlSinglePage', 'builtin');
expect(result2).toBe('lobe-web-browsing____crawlSinglePage____builtin');
expect(result2).toBe('lobe-web-browsing____crawlSinglePage');
});
it('should handle plugin tools correctly', () => {
@@ -167,7 +173,7 @@ describe('ToolNameResolver', () => {
{
function: {
arguments: '{"query": "test"}',
name: 'test-plugin____myAction____builtin',
name: 'test-plugin____myAction',
},
id: 'call_1',
type: 'function',
@@ -195,7 +201,7 @@ describe('ToolNameResolver', () => {
});
});
it('should handle default type correctly', () => {
it('should fall back to builtin type for two-segment tool names without manifest', () => {
const toolCalls = [
{
function: {
@@ -217,7 +223,34 @@ describe('ToolNameResolver', () => {
const result = resolver.resolve(toolCalls, manifests);
expect(result[0].type).toBe('default');
expect(result[0].type).toBe('builtin');
});
it('should still parse legacy three-segment ____builtin tool names', () => {
const toolCalls = [
{
function: {
arguments: '{}',
name: 'legacy-plugin____legacyAction____builtin',
},
id: 'call_1',
type: 'function',
},
];
const manifests = {
'legacy-plugin': {
api: [{ description: '', name: 'legacyAction', parameters: {} }],
identifier: 'legacy-plugin',
meta: {},
type: 'builtin' as const,
},
};
const result = resolver.resolve(toolCalls, manifests);
expect(result[0].type).toBe('builtin');
expect(result[0].apiName).toBe('legacyAction');
});
it('should handle empty tool calls array', () => {
@@ -233,7 +266,7 @@ describe('ToolNameResolver', () => {
type: 'function',
},
{
function: { arguments: '{}', name: 'plugin2____action2____builtin' },
function: { arguments: '{}', name: 'plugin2____action2' },
id: 'call_2',
type: 'function',
},
@@ -487,7 +520,7 @@ describe('ToolNameResolver', () => {
{
function: {
arguments: '{"query": "test"}',
name: 'test-plugin____myAction____builtin',
name: 'test-plugin____myAction',
},
id: 'call_1',
thoughtSignature: 'thinking about this...',
@@ -515,7 +548,7 @@ describe('ToolNameResolver', () => {
{
function: {
arguments: '{"query": "test"}',
name: 'test-plugin____myAction____builtin',
name: 'test-plugin____myAction',
},
id: 'call_1',
type: 'function',
@@ -559,7 +592,7 @@ describe('ToolNameResolver', () => {
it('should handle tool calls with different types', () => {
const toolCalls = [
{
function: { arguments: '{}', name: 'plugin1____action1____builtin' },
function: { arguments: '{}', name: 'plugin1____action1' },
id: 'call_1',
type: 'function',
},
@@ -129,7 +129,7 @@ describe('ToolsEngine', () => {
{
type: 'function',
function: {
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
description: 'Search the web',
parameters: {
type: 'object',
@@ -203,7 +203,7 @@ describe('ToolsEngine', () => {
{
type: 'function',
function: {
name: 'lobe-all-optional____search____builtin',
name: 'lobe-all-optional____search',
description: 'Search with all-optional params',
parameters: {
type: 'object',
@@ -715,7 +715,7 @@ describe('ToolsEngine', () => {
{
type: 'function',
function: {
name: 'builtin-1____builtin-api-1____builtin',
name: 'builtin-1____builtin-api-1',
description: 'Builtin API 1',
parameters: {},
},
@@ -1222,8 +1222,8 @@ describe('ToolsEngine', () => {
// Should only generate 2 tools, not 3
expect(result).toHaveLength(2);
expect(result![0].function.name).toBe('lobe-web-browsing____search____builtin');
expect(result![1].function.name).toBe('dalle____generateImage____builtin');
expect(result![0].function.name).toBe('lobe-web-browsing____search');
expect(result![1].function.name).toBe('dalle____generateImage');
});
it('should deduplicate between toolIds and defaultToolIds', () => {
@@ -1242,8 +1242,8 @@ describe('ToolsEngine', () => {
// Should only generate 2 tools (lobe-web-browsing should appear once)
expect(result).toHaveLength(2);
expect(result![0].function.name).toBe('lobe-web-browsing____search____builtin');
expect(result![1].function.name).toBe('dalle____generateImage____builtin');
expect(result![0].function.name).toBe('lobe-web-browsing____search');
expect(result![1].function.name).toBe('dalle____generateImage');
});
it('should deduplicate in generateToolsDetailed', () => {
@@ -1455,7 +1455,7 @@ describe('ToolsEngine', () => {
// Should only include dalle, not the default lobe-web-browsing
expect(result).toHaveLength(1);
expect(result![0].function.name).toBe('dalle____generateImage____builtin');
expect(result![0].function.name).toBe('dalle____generateImage');
});
it('should not include default tools when skipDefaultTools is true in generateToolsDetailed', () => {
@@ -1575,7 +1575,7 @@ describe('ToolsEngine', () => {
// Should only include dalle
expect(result).toHaveLength(1);
expect(result![0].function.name).toBe('dalle____generateImage____builtin');
expect(result![0].function.name).toBe('dalle____generateImage');
});
});
});
@@ -39,14 +39,14 @@ describe('PlaceholderVariablesProcessor — tool message substitution', () => {
{
id: 'toolu_1',
type: 'function',
function: { name: 'lobe-activator____activateSkill____builtin', arguments: '{}' },
function: { name: 'lobe-activator____activateSkill', arguments: '{}' },
},
],
},
{
role: 'tool',
tool_call_id: 'toolu_1',
name: 'lobe-activator____activateSkill____builtin',
name: 'lobe-activator____activateSkill',
content:
'<lobehub_platform_guides>\n| Agent ID | `{{agent_id}}` |\n| Agent Title | {{agent_title}} |\n| Topic ID | `{{topic_id}}` |\n</lobehub_platform_guides>',
},
@@ -48,7 +48,7 @@ describe('ToolMessageReorder', () => {
function: {
arguments:
'{"query":"LobeChat","searchEngines":["brave","google","duckduckgo","qwant"]}',
name: 'lobe-web-browsing____searchWithSearXNG____builtin',
name: 'lobe-web-browsing____searchWithSearXNG',
},
id: 'call_6xCmrOtFOyBAcqpqO1TGfw2B',
type: 'function',
@@ -57,7 +57,7 @@ describe('ToolMessageReorder', () => {
function: {
arguments:
'{"query":"LobeChat","searchEngines":["brave","google","duckduckgo","qwant"]}',
name: 'lobe-web-browsing____searchWithSearXNG____builtin',
name: 'lobe-web-browsing____searchWithSearXNG',
},
id: 'tool_call_nXxXHW8Z',
type: 'function',
@@ -66,7 +66,7 @@ describe('ToolMessageReorder', () => {
},
{
content: '[]',
name: 'lobe-web-browsing____searchWithSearXNG____builtin',
name: 'lobe-web-browsing____searchWithSearXNG',
role: 'tool',
tool_call_id: 'call_6xCmrOtFOyBAcqpqO1TGfw2B',
},
@@ -76,13 +76,13 @@ describe('ToolMessageReorder', () => {
},
{
content: '[]',
name: 'lobe-web-browsing____searchWithSearXNG____builtin',
name: 'lobe-web-browsing____searchWithSearXNG',
role: 'tool',
tool_call_id: 'tool_call_nXxXHW8Z',
},
{
content: '[]',
name: 'lobe-web-browsing____searchWithSearXNG____builtin',
name: 'lobe-web-browsing____searchWithSearXNG',
role: 'tool',
tool_call_id: 'tool_call_2f3CEKz9',
},
@@ -109,7 +109,7 @@ describe('ToolMessageReorder', () => {
function: {
arguments:
'{"query":"LobeChat","searchEngines":["brave","google","duckduckgo","qwant"]}',
name: 'lobe-web-browsing____searchWithSearXNG____builtin',
name: 'lobe-web-browsing____searchWithSearXNG',
},
id: 'call_6xCmrOtFOyBAcqpqO1TGfw2B',
type: 'function',
@@ -118,7 +118,7 @@ describe('ToolMessageReorder', () => {
function: {
arguments:
'{"query":"LobeChat","searchEngines":["brave","google","duckduckgo","qwant"]}',
name: 'lobe-web-browsing____searchWithSearXNG____builtin',
name: 'lobe-web-browsing____searchWithSearXNG',
},
id: 'tool_call_nXxXHW8Z',
type: 'function',
@@ -127,13 +127,13 @@ describe('ToolMessageReorder', () => {
},
{
content: '[]',
name: 'lobe-web-browsing____searchWithSearXNG____builtin',
name: 'lobe-web-browsing____searchWithSearXNG',
role: 'tool',
tool_call_id: 'call_6xCmrOtFOyBAcqpqO1TGfw2B',
},
{
content: '[]',
name: 'lobe-web-browsing____searchWithSearXNG____builtin',
name: 'lobe-web-browsing____searchWithSearXNG',
role: 'tool',
tool_call_id: 'tool_call_nXxXHW8Z',
},
@@ -249,7 +249,7 @@ const formatMentionedAgentsContext = (mentionedAgents: RuntimeMentionedAgent[]):
.join('\n');
return `<mentioned_agents>
<instruction>The user has @mentioned the following agent(s) in their message. You MUST call the \`lobe-agent-management____callAgent____builtin\` tool to delegate the user's request to the mentioned agent. Do NOT attempt to handle the request yourself — call the agent and let them respond.</instruction>
<instruction>The user has @mentioned the following agent(s) in their message. You MUST call the \`lobe-agent-management____callAgent\` tool to delegate the user's request to the mentioned agent. Do NOT attempt to handle the request yourself — call the agent and let them respond.</instruction>
${agentsXml}
</mentioned_agents>`;
};
@@ -74,7 +74,7 @@ export class OnboardingSyntheticStateInjector extends BaseProcessor {
{
function: {
arguments: '{}',
name: 'lobe-web-onboarding____getOnboardingState____builtin',
name: 'lobe-web-onboarding____getOnboardingState',
},
id: toolCallId,
type: 'function',
@@ -93,7 +93,7 @@ describe('AgentManagementContextInjector', () => {
expect(delegationMsg.content).toContain('agt_designer');
expect(delegationMsg.content).toContain('Designer Agent');
expect(delegationMsg.content).toContain(
'MUST call the `lobe-agent-management____callAgent____builtin` tool',
'MUST call the `lobe-agent-management____callAgent` tool',
);
expect(delegationMsg.meta.injectType).toBe('agent-mention-delegation');
});
@@ -388,7 +388,7 @@ describe('google contextBuilders', () => {
{
function: {
arguments: '{"query":"杭州天气","searchEngines":["google"]}',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
},
id: 'call_001',
type: 'function',
@@ -397,7 +397,7 @@ describe('google contextBuilders', () => {
},
{
content: 'Tool execution was aborted by user.',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
role: 'tool',
tool_call_id: 'call_001',
},
@@ -408,7 +408,7 @@ describe('google contextBuilders', () => {
{
function: {
arguments: '{"query":"杭州 天气","searchEngines":["bing"]}',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
},
id: 'call_002',
type: 'function',
@@ -417,7 +417,7 @@ describe('google contextBuilders', () => {
},
{
content: 'no result',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
role: 'tool',
tool_call_id: 'call_002',
},
@@ -444,7 +444,7 @@ describe('google contextBuilders', () => {
{
functionCall: {
args: { query: '杭州天气', searchEngines: ['google'] },
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
},
thoughtSignature: GEMINI_MAGIC_THOUGHT_SIGNATURE,
},
@@ -455,7 +455,7 @@ describe('google contextBuilders', () => {
parts: [
{
functionResponse: {
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
response: { result: 'Tool execution was aborted by user.' },
},
},
@@ -467,7 +467,7 @@ describe('google contextBuilders', () => {
{
functionCall: {
args: { query: '杭州 天气', searchEngines: ['bing'] },
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
},
thoughtSignature: GEMINI_MAGIC_THOUGHT_SIGNATURE,
},
@@ -478,7 +478,7 @@ describe('google contextBuilders', () => {
parts: [
{
functionResponse: {
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
response: { result: 'no result' },
},
},
@@ -502,7 +502,7 @@ describe('google contextBuilders', () => {
{
function: {
arguments: '{"query":"杭州天气","searchEngines":["google"]}',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
},
id: 'call_001',
thoughtSignature: existingSignature,
@@ -512,7 +512,7 @@ describe('google contextBuilders', () => {
},
{
content: 'Tool result',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
role: 'tool',
tool_call_id: 'call_001',
},
@@ -530,7 +530,7 @@ describe('google contextBuilders', () => {
{
functionCall: {
args: { query: '杭州天气', searchEngines: ['google'] },
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
},
// Should keep existing thoughtSignature, not add magic signature
thoughtSignature: existingSignature,
@@ -542,7 +542,7 @@ describe('google contextBuilders', () => {
parts: [
{
functionResponse: {
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
response: { result: 'Tool result' },
},
},
@@ -682,7 +682,7 @@ describe('google contextBuilders', () => {
{
function: {
arguments: '{"query":"杭州天气","searchEngines":["google"]}',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
},
id: 'call_001',
type: 'function',
@@ -691,7 +691,7 @@ describe('google contextBuilders', () => {
},
{
content: 'Tool execution was aborted by user.',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
role: 'tool',
tool_call_id: 'call_001',
},
@@ -722,7 +722,7 @@ describe('google contextBuilders', () => {
{
functionCall: {
args: { query: '杭州天气', searchEngines: ['google'] },
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
},
// No thoughtSignature should be added when last message is user text
},
@@ -733,7 +733,7 @@ describe('google contextBuilders', () => {
parts: [
{
functionResponse: {
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
response: { result: 'Tool execution was aborted by user.' },
},
},
@@ -1341,7 +1341,7 @@ describe('google contextBuilders', () => {
{
function: {
description: 'Search the web',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
parameters: {
properties: { query: { type: 'string' } },
required: ['query'],
@@ -1365,7 +1365,7 @@ describe('google contextBuilders', () => {
{
function: {
description: 'Search the web (duplicate)',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
parameters: {
properties: { query: { type: 'string' } },
required: ['query'],
@@ -1380,9 +1380,7 @@ describe('google contextBuilders', () => {
expect(googleTools).toHaveLength(1);
expect(googleTools![0].functionDeclarations).toHaveLength(2);
expect(googleTools![0].functionDeclarations![0].name).toBe(
'lobe-web-browsing____search____builtin',
);
expect(googleTools![0].functionDeclarations![0].name).toBe('lobe-web-browsing____search');
expect(googleTools![0].functionDeclarations![0].description).toBe('Search the web');
expect(googleTools![0].functionDeclarations![1].name).toBe('get_weather');
});
@@ -1391,7 +1391,7 @@ describe('GoogleGenerativeAIStream', () => {
parts: [
{
functionCall: {
name: 'lobe-gtd____createPlan____builtin',
name: 'lobe-gtd____createPlan',
args: {
goal: 'Fix Linear API Argument Validation Error',
description: 'Investigate the Linear API error.',
@@ -1424,7 +1424,7 @@ describe('GoogleGenerativeAIStream', () => {
parts: [
{
functionCall: {
name: 'lobe-gtd____createTodos____builtin',
name: 'lobe-gtd____createTodos',
args: {
adds: [
'Verify Linear GraphQL API requirements',
@@ -1498,12 +1498,12 @@ describe('GoogleGenerativeAIStream', () => {
// First tool call (createPlan)
'id: chat_test',
'event: tool_calls',
'data: [{"function":{"arguments":"{\\"goal\\":\\"Fix Linear API Argument Validation Error\\",\\"description\\":\\"Investigate the Linear API error.\\",\\"context\\":\\"The user is encountering a validation error.\\"}","name":"lobe-gtd____createPlan____builtin"},"id":"lobe-gtd____createPlan____builtin_0_tool_id_1","index":0,"thoughtSignature":"EoIYCv8XAXLI2nx+C18votz5l0A...","type":"function"}]\n',
'data: [{"function":{"arguments":"{\\"goal\\":\\"Fix Linear API Argument Validation Error\\",\\"description\\":\\"Investigate the Linear API error.\\",\\"context\\":\\"The user is encountering a validation error.\\"}","name":"lobe-gtd____createPlan"},"id":"lobe-gtd____createPlan_0_tool_id_1","index":0,"thoughtSignature":"EoIYCv8XAXLI2nx+C18votz5l0A...","type":"function"}]\n',
// Second tool call (createTodos) - should be a SEPARATE event with index:0
'id: chat_test',
'event: tool_calls',
'data: [{"function":{"arguments":"{\\"adds\\":[\\"Verify Linear GraphQL API requirements\\",\\"Determine if code needs to look up Team UUID\\",\\"Provide corrected code\\"]}","name":"lobe-gtd____createTodos____builtin"},"id":"lobe-gtd____createTodos____builtin_0_tool_id_2","index":0,"type":"function"}]\n',
'data: [{"function":{"arguments":"{\\"adds\\":[\\"Verify Linear GraphQL API requirements\\",\\"Determine if code needs to look up Team UUID\\",\\"Provide corrected code\\"]}","name":"lobe-gtd____createTodos"},"id":"lobe-gtd____createTodos_0_tool_id_2","index":0,"type":"function"}]\n',
// Stop and usage
'id: chat_test',
@@ -13,7 +13,7 @@ exports[`OpenAIResponsesStream > Reasoning > summary 1`] = `
",
"event: data
",
"data: {"type":"response.in_progress","response":{"id":"resp_684313b89200819087f27686e0c822260b502bf083132d0d","object":"response","created_at":1749226424,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"o4-mini","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"medium","summary":"detailed"},"service_tier":"auto","store":false,"temperature":1,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"function","description":"a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results","name":"lobe-web-browsing____search____builtin","parameters":{"properties":{"query":{"description":"The search query","type":"string"},"searchCategories":{"description":"The search categories you can set:","items":{"enum":["general","images","news","science","videos"],"type":"string"},"type":"array"},"searchEngines":{"description":"The search engines you can use:","items":{"enum":["google","bilibili","bing","duckduckgo","npm","pypi","github","arxiv","google scholar","z-library","reddit","imdb","brave","wikipedia","pinterest","unsplash","vimeo","youtube"],"type":"string"},"type":"array"},"searchTimeRange":{"description":"The time range you can set:","enum":["anytime","day","week","month","year"],"type":"string"}},"required":["query"],"type":"object"},"strict":true},{"type":"function","description":"A crawler can visit page content. Output is a JSON object of title, content, url and website","name":"lobe-web-browsing____crawlSinglePage____builtin","parameters":{"properties":{"url":{"description":"The url need to be crawled","type":"string"}},"required":["url"],"type":"object"},"strict":true},{"type":"function","description":"A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website","name":"lobe-web-browsing____crawlMultiPages____builtin","parameters":{"properties":{"urls":{"items":{"description":"The urls need to be crawled","type":"string"},"type":"array"}},"required":["urls"],"type":"object"},"strict":true}],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
"data: {"type":"response.in_progress","response":{"id":"resp_684313b89200819087f27686e0c822260b502bf083132d0d","object":"response","created_at":1749226424,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"o4-mini","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"medium","summary":"detailed"},"service_tier":"auto","store":false,"temperature":1,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"function","description":"a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results","name":"lobe-web-browsing____search","parameters":{"properties":{"query":{"description":"The search query","type":"string"},"searchCategories":{"description":"The search categories you can set:","items":{"enum":["general","images","news","science","videos"],"type":"string"},"type":"array"},"searchEngines":{"description":"The search engines you can use:","items":{"enum":["google","bilibili","bing","duckduckgo","npm","pypi","github","arxiv","google scholar","z-library","reddit","imdb","brave","wikipedia","pinterest","unsplash","vimeo","youtube"],"type":"string"},"type":"array"},"searchTimeRange":{"description":"The time range you can set:","enum":["anytime","day","week","month","year"],"type":"string"}},"required":["query"],"type":"object"},"strict":true},{"type":"function","description":"A crawler can visit page content. Output is a JSON object of title, content, url and website","name":"lobe-web-browsing____crawlSinglePage","parameters":{"properties":{"url":{"description":"The url need to be crawled","type":"string"}},"required":["url"],"type":"object"},"strict":true},{"type":"function","description":"A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website","name":"lobe-web-browsing____crawlMultiPages","parameters":{"properties":{"urls":{"items":{"description":"The urls need to be crawled","type":"string"},"type":"array"}},"required":["urls"],"type":"object"},"strict":true}],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
",
"id: resp_684313b89200819087f27686e0c822260b502bf083132d0d
@@ -559,7 +559,7 @@ exports[`OpenAIResponsesStream > should transform OpenAI stream to protocol stre
",
"event: data
",
"data: {"type":"response.in_progress","response":{"id":"resp_683e7b8ca3308190b6837f20d2c015cd0cf93af363cdcf58","object":"response","created_at":1748925324,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"o4-mini","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"medium","summary":null},"service_tier":"auto","store":false,"temperature":1,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"function","description":"a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results","name":"lobe-web-browsing____search____builtin","parameters":{"properties":{"query":{"description":"The search query","type":"string"},"searchCategories":{"description":"The search categories you can set:","items":{"enum":["general","images","news","science","videos"],"type":"string"},"type":"array"},"searchEngines":{"description":"The search engines you can use:","items":{"enum":["google","bilibili","bing","duckduckgo","npm","pypi","github","arxiv","google scholar","z-library","reddit","imdb","brave","wikipedia","pinterest","unsplash","vimeo","youtube"],"type":"string"},"type":"array"},"searchTimeRange":{"description":"The time range you can set:","enum":["anytime","day","week","month","year"],"type":"string"}},"required":["query"],"type":"object"},"strict":true},{"type":"function","description":"A crawler can visit page content. Output is a JSON object of title, content, url and website","name":"lobe-web-browsing____crawlSinglePage____builtin","parameters":{"properties":{"url":{"description":"The url need to be crawled","type":"string"}},"required":["url"],"type":"object"},"strict":true},{"type":"function","description":"A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website","name":"lobe-web-browsing____crawlMultiPages____builtin","parameters":{"properties":{"urls":{"items":{"description":"The urls need to be crawled","type":"string"},"type":"array"}},"required":["urls"],"type":"object"},"strict":true}],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
"data: {"type":"response.in_progress","response":{"id":"resp_683e7b8ca3308190b6837f20d2c015cd0cf93af363cdcf58","object":"response","created_at":1748925324,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"o4-mini","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"medium","summary":null},"service_tier":"auto","store":false,"temperature":1,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"function","description":"a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results","name":"lobe-web-browsing____search","parameters":{"properties":{"query":{"description":"The search query","type":"string"},"searchCategories":{"description":"The search categories you can set:","items":{"enum":["general","images","news","science","videos"],"type":"string"},"type":"array"},"searchEngines":{"description":"The search engines you can use:","items":{"enum":["google","bilibili","bing","duckduckgo","npm","pypi","github","arxiv","google scholar","z-library","reddit","imdb","brave","wikipedia","pinterest","unsplash","vimeo","youtube"],"type":"string"},"type":"array"},"searchTimeRange":{"description":"The time range you can set:","enum":["anytime","day","week","month","year"],"type":"string"}},"required":["query"],"type":"object"},"strict":true},{"type":"function","description":"A crawler can visit page content. Output is a JSON object of title, content, url and website","name":"lobe-web-browsing____crawlSinglePage","parameters":{"properties":{"url":{"description":"The url need to be crawled","type":"string"}},"required":["url"],"type":"object"},"strict":true},{"type":"function","description":"A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website","name":"lobe-web-browsing____crawlMultiPages","parameters":{"properties":{"urls":{"items":{"description":"The urls need to be crawled","type":"string"},"type":"array"}},"required":["urls"],"type":"object"},"strict":true}],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
",
"id: resp_683e7b8ca3308190b6837f20d2c015cd0cf93af363cdcf58
@@ -34,7 +34,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
parameters: {
properties: {
query: { description: 'The search query', type: 'string' },
@@ -88,7 +88,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'A crawler can visit page content. Output is a JSON object of title, content, url and website',
name: 'lobe-web-browsing____crawlSinglePage____builtin',
name: 'lobe-web-browsing____crawlSinglePage',
parameters: {
properties: { url: { description: 'The url need to be crawled', type: 'string' } },
required: ['url'],
@@ -100,7 +100,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website',
name: 'lobe-web-browsing____crawlMultiPages____builtin',
name: 'lobe-web-browsing____crawlMultiPages',
parameters: {
properties: {
urls: {
@@ -147,7 +147,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
parameters: {
properties: {
query: { description: 'The search query', type: 'string' },
@@ -201,7 +201,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'A crawler can visit page content. Output is a JSON object of title, content, url and website',
name: 'lobe-web-browsing____crawlSinglePage____builtin',
name: 'lobe-web-browsing____crawlSinglePage',
parameters: {
properties: { url: { description: 'The url need to be crawled', type: 'string' } },
required: ['url'],
@@ -213,7 +213,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website',
name: 'lobe-web-browsing____crawlMultiPages____builtin',
name: 'lobe-web-browsing____crawlMultiPages',
parameters: {
properties: {
urls: {
@@ -869,7 +869,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
parameters: {
properties: {
query: { description: 'The search query', type: 'string' },
@@ -923,7 +923,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'A crawler can visit page content. Output is a JSON object of title, content, url and website',
name: 'lobe-web-browsing____crawlSinglePage____builtin',
name: 'lobe-web-browsing____crawlSinglePage',
parameters: {
properties: {
url: { description: 'The url need to be crawled', type: 'string' },
@@ -937,7 +937,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website',
name: 'lobe-web-browsing____crawlMultiPages____builtin',
name: 'lobe-web-browsing____crawlMultiPages',
parameters: {
properties: {
urls: {
@@ -984,7 +984,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
parameters: {
properties: {
query: { description: 'The search query', type: 'string' },
@@ -1038,7 +1038,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'A crawler can visit page content. Output is a JSON object of title, content, url and website',
name: 'lobe-web-browsing____crawlSinglePage____builtin',
name: 'lobe-web-browsing____crawlSinglePage',
parameters: {
properties: {
url: { description: 'The url need to be crawled', type: 'string' },
@@ -1052,7 +1052,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website',
name: 'lobe-web-browsing____crawlMultiPages____builtin',
name: 'lobe-web-browsing____crawlMultiPages',
parameters: {
properties: {
urls: {
@@ -1256,7 +1256,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results',
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
parameters: {
properties: {
query: { description: 'The search query', type: 'string' },
@@ -1310,7 +1310,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'A crawler can visit page content. Output is a JSON object of title, content, url and website',
name: 'lobe-web-browsing____crawlSinglePage____builtin',
name: 'lobe-web-browsing____crawlSinglePage',
parameters: {
properties: {
url: { description: 'The url need to be crawled', type: 'string' },
@@ -1324,7 +1324,7 @@ describe('OpenAIResponsesStream', () => {
type: 'function',
description:
'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website',
name: 'lobe-web-browsing____crawlMultiPages____builtin',
name: 'lobe-web-browsing____crawlMultiPages',
parameters: {
properties: {
urls: {
@@ -227,8 +227,8 @@ export class ResponsesService extends BaseService {
/**
* Decode internal tool name format to display name.
* - lobe-client-fn____get_weather → get_weather
* - lobe-cloud-sandbox____executeCode____builtin → lobe-cloud-sandbox/executeCode
* - my-plugin____myApi → my-plugin/myApi
* - lobe-cloud-sandbox____executeCode → lobe-cloud-sandbox/executeCode
* - my-plugin____myApi____mcp → my-plugin/myApi (legacy 3-segment still tolerated)
*/
private decodeToolName(rawName: string): string {
const SEPARATOR = '____';
+3 -3
View File
@@ -35,15 +35,15 @@ describe('dedupeBy', () => {
it('should deduplicate tools by function name (real-world scenario)', () => {
const tools = [
{ function: { name: 'lobe-web-browsing____search____builtin' }, type: 'function' },
{ function: { name: 'lobe-web-browsing____search' }, type: 'function' },
{ function: { name: 'get_weather' }, type: 'function' },
{ function: { name: 'lobe-web-browsing____search____builtin' }, type: 'function' },
{ function: { name: 'lobe-web-browsing____search' }, type: 'function' },
];
const result = dedupeBy(tools, (tool) => tool.function.name);
expect(result).toHaveLength(2);
expect(result[0].function.name).toBe('lobe-web-browsing____search____builtin');
expect(result[0].function.name).toBe('lobe-web-browsing____search');
expect(result[1].function.name).toBe('get_weather');
});
});
+2 -2
View File
@@ -144,7 +144,7 @@ describe('toolEngineering', () => {
expect(result![0]).toMatchObject({
function: {
description: 'Search the web',
name: 'search____search____builtin',
name: 'search____search',
parameters: {
properties: {
query: { description: 'Search query', type: 'string' },
@@ -368,7 +368,7 @@ describe('toolEngineering', () => {
const result = getEnabledTools(['search'], 'gpt-4', 'openai');
expect(result).toHaveLength(1);
expect(result[0]).toHaveProperty('type', 'function');
expect(result[0].function).toHaveProperty('name', 'search____search____builtin');
expect(result[0].function).toHaveProperty('name', 'search____search');
});
it('should use provided model and provider', () => {
@@ -427,7 +427,7 @@ describe('execAgent', () => {
item: {
type: 'function_call',
call_id: toolCallId,
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
arguments: JSON.stringify({ query: '杭州天气' }),
},
},
@@ -450,7 +450,7 @@ describe('execAgent', () => {
{
type: 'function_call',
call_id: toolCallId,
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
arguments: JSON.stringify({ query: '杭州天气' }),
},
],
@@ -91,7 +91,7 @@ const createMockResponseWithMultipleTools = (roundNum: number) => {
item: {
type: 'function_call',
call_id: toolCallId1,
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
arguments: JSON.stringify({ query: `query_round${roundNum}` }),
},
},
@@ -101,7 +101,7 @@ const createMockResponseWithMultipleTools = (roundNum: number) => {
item: {
type: 'function_call',
call_id: toolCallId2,
name: 'lobe-web-browsing____crawl____builtin',
name: 'lobe-web-browsing____crawl',
arguments: JSON.stringify({ url: `https://example.com/page${roundNum}` }),
},
},
@@ -129,13 +129,13 @@ const createMockResponseWithMultipleTools = (roundNum: number) => {
{
type: 'function_call',
call_id: toolCallId1,
name: 'lobe-web-browsing____search____builtin',
name: 'lobe-web-browsing____search',
arguments: JSON.stringify({ query: `query_round${roundNum}` }),
},
{
type: 'function_call',
call_id: toolCallId2,
name: 'lobe-web-browsing____crawl____builtin',
name: 'lobe-web-browsing____crawl',
arguments: JSON.stringify({ url: `https://example.com/page${roundNum}` }),
},
],
+1 -1
View File
@@ -33,7 +33,7 @@ describe('web onboarding tool result helpers', () => {
expect(message).toContain('Structured fields still needed: interests.');
expect(message).toContain('Phase: Discovery');
expect(message).toContain(
'Questioning rule: prefer the `lobe-user-interaction____askUserQuestion____builtin` tool call for structured collection or explicit UI input. For natural exploratory questions, plain text is allowed.',
'Questioning rule: prefer the `lobe-user-interaction____askUserQuestion` tool call for structured collection or explicit UI input. For natural exploratory questions, plain text is allowed.',
);
});