🐛 fix: support memory tools run in server (#12471)

* support server memory run

* refactor builtin tools list

* refactor builtin tools list

* add lobe-tools

* fix lint
This commit is contained in:
Arvin Xu 2026-02-25 22:13:02 +08:00 committed by GitHub
parent 5371507b22
commit 5a30c9a14f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 1449 additions and 337 deletions

View file

@ -198,7 +198,9 @@
"@lobechat/builtin-tool-notebook": "workspace:*",
"@lobechat/builtin-tool-page-agent": "workspace:*",
"@lobechat/builtin-tool-skills": "workspace:*",
"@lobechat/builtin-tool-tools": "workspace:*",
"@lobechat/builtin-tool-web-browsing": "workspace:*",
"@lobechat/builtin-tools": "workspace:*",
"@lobechat/business-config": "workspace:*",
"@lobechat/business-const": "workspace:*",
"@lobechat/config": "workspace:*",

View file

@ -6,7 +6,8 @@
"exports": {
".": "./src/index.ts",
"./client": "./src/client/index.ts",
"./executor": "./src/executor/index.ts"
"./executor": "./src/executor/index.ts",
"./executionRuntime": "./src/ExecutionRuntime/index.ts"
},
"main": "./src/index.ts",
"scripts": {

View file

@ -0,0 +1,259 @@
import type {
ActivityMemoryItemSchema,
AddIdentityActionSchema,
ContextMemoryItemSchema,
ExperienceMemoryItemSchema,
PreferenceMemoryItemSchema,
RemoveIdentityActionSchema,
UpdateIdentityActionSchema,
} from '@lobechat/memory-user-memory/schemas';
import { formatMemorySearchResults } from '@lobechat/prompts';
import type {
AddActivityMemoryResult,
AddContextMemoryResult,
AddExperienceMemoryResult,
AddIdentityMemoryResult,
AddPreferenceMemoryResult,
BuiltinServerRuntimeOutput,
RemoveIdentityMemoryResult,
SearchMemoryParams,
SearchMemoryResult,
UpdateIdentityMemoryResult,
} from '@lobechat/types';
import type { z } from 'zod';
export interface MemoryRuntimeService {
addActivityMemory: (
params: z.infer<typeof ActivityMemoryItemSchema>,
) => Promise<AddActivityMemoryResult>;
addContextMemory: (
params: z.infer<typeof ContextMemoryItemSchema>,
) => Promise<AddContextMemoryResult>;
addExperienceMemory: (
params: z.infer<typeof ExperienceMemoryItemSchema>,
) => Promise<AddExperienceMemoryResult>;
addIdentityMemory: (
params: z.infer<typeof AddIdentityActionSchema>,
) => Promise<AddIdentityMemoryResult>;
addPreferenceMemory: (
params: z.infer<typeof PreferenceMemoryItemSchema>,
) => Promise<AddPreferenceMemoryResult>;
removeIdentityMemory: (
params: z.infer<typeof RemoveIdentityActionSchema>,
) => Promise<RemoveIdentityMemoryResult>;
searchMemory: (params: SearchMemoryParams) => Promise<SearchMemoryResult>;
updateIdentityMemory: (
params: z.infer<typeof UpdateIdentityActionSchema>,
) => Promise<UpdateIdentityMemoryResult>;
}
export type MemoryToolPermission = 'read-only' | 'read-write';
export interface MemoryExecutionRuntimeOptions {
service: MemoryRuntimeService;
toolPermission?: MemoryToolPermission;
}
const READ_ONLY_RESULT: BuiltinServerRuntimeOutput = {
content: 'Memory tool is in read-only mode for this chat',
success: false,
};
export class MemoryExecutionRuntime {
private service: MemoryRuntimeService;
private toolPermission: MemoryToolPermission;
constructor(options: MemoryExecutionRuntimeOptions) {
this.service = options.service;
this.toolPermission = options.toolPermission ?? 'read-write';
}
private get isReadOnly() {
return this.toolPermission === 'read-only';
}
async searchUserMemory(params: SearchMemoryParams): Promise<BuiltinServerRuntimeOutput> {
try {
const result = await this.service.searchMemory(params);
return {
content: formatMemorySearchResults({ query: params.query, results: result }),
state: result,
success: true,
};
} catch (e) {
return {
content: `searchUserMemory with error detail: ${(e as Error).message}`,
success: false,
};
}
}
async addContextMemory(
params: z.infer<typeof ContextMemoryItemSchema>,
): Promise<BuiltinServerRuntimeOutput> {
if (this.isReadOnly) return READ_ONLY_RESULT;
try {
const result = await this.service.addContextMemory(params);
if (!result.success) {
return { content: result.message, success: false };
}
return {
content: `Context memory "${params.title}" saved with memoryId: "${result.memoryId}" and contextId: "${result.contextId}"`,
state: { contextId: result.contextId, memoryId: result.memoryId },
success: true,
};
} catch (e) {
return {
content: `addContextMemory with error detail: ${(e as Error).message}`,
success: false,
};
}
}
async addActivityMemory(
params: z.infer<typeof ActivityMemoryItemSchema>,
): Promise<BuiltinServerRuntimeOutput> {
if (this.isReadOnly) return READ_ONLY_RESULT;
try {
const result = await this.service.addActivityMemory(params);
if (!result.success) {
return { content: result.message, success: false };
}
return {
content: `Activity memory "${params.title}" saved with memoryId: "${result.memoryId}" and activityId: "${result.activityId}"`,
state: { activityId: result.activityId, memoryId: result.memoryId },
success: true,
};
} catch (e) {
return {
content: `addActivityMemory with error detail: ${(e as Error).message}`,
success: false,
};
}
}
async addExperienceMemory(
params: z.infer<typeof ExperienceMemoryItemSchema>,
): Promise<BuiltinServerRuntimeOutput> {
if (this.isReadOnly) return READ_ONLY_RESULT;
try {
const result = await this.service.addExperienceMemory(params);
if (!result.success) {
return { content: result.message, success: false };
}
return {
content: `Experience memory "${params.title}" saved with memoryId: "${result.memoryId}" and experienceId: "${result.experienceId}"`,
state: { experienceId: result.experienceId, memoryId: result.memoryId },
success: true,
};
} catch (e) {
return {
content: `addExperienceMemory with error detail: ${(e as Error).message}`,
success: false,
};
}
}
async addIdentityMemory(
params: z.infer<typeof AddIdentityActionSchema>,
): Promise<BuiltinServerRuntimeOutput> {
if (this.isReadOnly) return READ_ONLY_RESULT;
try {
const result = await this.service.addIdentityMemory(params);
if (!result.success) {
return { content: result.message, success: false };
}
return {
content: `Identity memory "${params.title}" saved with memoryId: "${result.memoryId}" and identityId: "${result.identityId}"`,
state: { identityId: result.identityId, memoryId: result.memoryId },
success: true,
};
} catch (e) {
return {
content: `addIdentityMemory with error detail: ${(e as Error).message}`,
success: false,
};
}
}
async addPreferenceMemory(
params: z.infer<typeof PreferenceMemoryItemSchema>,
): Promise<BuiltinServerRuntimeOutput> {
if (this.isReadOnly) return READ_ONLY_RESULT;
try {
const result = await this.service.addPreferenceMemory(params);
if (!result.success) {
return { content: result.message, success: false };
}
return {
content: `Preference memory "${params.title}" saved with memoryId: "${result.memoryId}" and preferenceId: "${result.preferenceId}"`,
state: { memoryId: result.memoryId, preferenceId: result.preferenceId },
success: true,
};
} catch (e) {
return {
content: `addPreferenceMemory with error detail: ${(e as Error).message}`,
success: false,
};
}
}
async updateIdentityMemory(
params: z.infer<typeof UpdateIdentityActionSchema>,
): Promise<BuiltinServerRuntimeOutput> {
if (this.isReadOnly) return READ_ONLY_RESULT;
try {
const result = await this.service.updateIdentityMemory(params);
if (!result.success) {
return { content: result.message, success: false };
}
return {
content: `Identity memory updated: ${params.id}`,
state: { identityId: params.id },
success: true,
};
} catch (e) {
return {
content: `updateIdentityMemory with error detail: ${(e as Error).message}`,
success: false,
};
}
}
async removeIdentityMemory(
params: z.infer<typeof RemoveIdentityActionSchema>,
): Promise<BuiltinServerRuntimeOutput> {
if (this.isReadOnly) return READ_ONLY_RESULT;
try {
const result = await this.service.removeIdentityMemory(params);
if (!result.success) {
return { content: result.message, success: false };
}
return {
content: `Identity memory removed: ${params.id}\nReason: ${params.reason}`,
state: { identityId: params.id, reason: params.reason },
success: true,
};
} catch (e) {
return {
content: `removeIdentityMemory with error detail: ${(e as Error).message}`,
success: false,
};
}
}
}

View file

@ -1,33 +1,35 @@
import {
type ActivityMemoryItemSchema,
type AddIdentityActionSchema,
type ContextMemoryItemSchema,
type ExperienceMemoryItemSchema,
type PreferenceMemoryItemSchema,
type RemoveIdentityActionSchema,
type UpdateIdentityActionSchema,
import type {
ActivityMemoryItemSchema,
AddIdentityActionSchema,
ContextMemoryItemSchema,
ExperienceMemoryItemSchema,
PreferenceMemoryItemSchema,
RemoveIdentityActionSchema,
UpdateIdentityActionSchema,
} from '@lobechat/memory-user-memory/schemas';
import { formatMemorySearchResults } from '@lobechat/prompts';
import { type BuiltinToolContext, type BuiltinToolResult, type SearchMemoryParams } from '@lobechat/types';
import type { BuiltinToolContext, BuiltinToolResult, SearchMemoryParams } from '@lobechat/types';
import { BaseExecutor } from '@lobechat/types';
import { type z } from 'zod';
import type { z } from 'zod';
import { userMemoryService } from '@/services/userMemory';
import { getAgentStoreState } from '@/store/agent';
import { agentChatConfigSelectors, chatConfigByIdSelectors } from '@/store/agent/selectors';
import { MemoryExecutionRuntime } from '../ExecutionRuntime';
import { MemoryIdentifier } from '../manifest';
import { MemoryApiName } from '../types';
/**
* Memory Tool Executor
*
* Handles all memory-related operations including search, add, update, and remove.
*/
class MemoryExecutor extends BaseExecutor<typeof MemoryApiName> {
readonly identifier = MemoryIdentifier;
protected readonly apiEnum = MemoryApiName;
private runtime: MemoryExecutionRuntime;
constructor() {
super();
this.runtime = new MemoryExecutionRuntime({ service: userMemoryService });
}
private resolveToolPermission = (agentId?: string): 'read-only' | 'read-write' => {
const state = getAgentStoreState();
if (!state) return 'read-write';
@ -56,323 +58,71 @@ class MemoryExecutor extends BaseExecutor<typeof MemoryApiName> {
return chatConfig?.memory?.effort;
};
// ==================== Search API ====================
/**
* Search user memories based on query
*/
searchUserMemory = async (
params: SearchMemoryParams,
ctx?: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
try {
const result = await userMemoryService.searchMemory({
...params,
effort: this.resolveMemoryEffort(ctx?.agentId),
});
return {
content: formatMemorySearchResults({ query: params.query, results: result }),
state: result,
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `searchUserMemory with error detail: ${err.message}`,
error: {
body: error,
message: err.message,
type: 'PluginServerError',
},
success: false,
};
}
return this.runtime.searchUserMemory({
...params,
effort: this.resolveMemoryEffort(ctx?.agentId),
});
};
// ==================== Add APIs ====================
/**
* Add a context memory
*/
addContextMemory = async (
params: z.infer<typeof ContextMemoryItemSchema>,
ctx?: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
try {
this.ensureWritable(ctx?.agentId);
const result = await userMemoryService.addContextMemory(params);
if (!result.success) {
return {
error: {
message: result.message,
type: 'PluginServerError',
},
success: false,
};
}
return {
content: `Context memory "${params.title}" saved with memoryId: "${result.memoryId}" and contextId: "${result.contextId}"`,
state: { contextId: result.contextId, memoryId: result.memoryId },
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `addContextMemory with error detail: ${err.message}`,
error: {
body: error,
message: err.message,
type: 'PluginServerError',
},
success: false,
};
}
this.ensureWritable(ctx?.agentId);
return this.runtime.addContextMemory(params);
};
/**
* Add an activity memory
*/
addActivityMemory = async (
params: z.infer<typeof ActivityMemoryItemSchema>,
ctx?: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
try {
this.ensureWritable(ctx?.agentId);
const result = await userMemoryService.addActivityMemory(params);
if (!result.success) {
return {
error: {
message: result.message,
type: 'PluginServerError',
},
success: false,
};
}
return {
content: `Activity memory "${params.title}" saved with memoryId: "${result.memoryId}" and activityId: "${result.activityId}"`,
state: { activityId: result.activityId, memoryId: result.memoryId },
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `addActivityMemory with error detail: ${err.message}`,
error: {
body: error,
message: err.message,
type: 'PluginServerError',
},
success: false,
};
}
this.ensureWritable(ctx?.agentId);
return this.runtime.addActivityMemory(params);
};
/**
* Add an experience memory
*/
addExperienceMemory = async (
params: z.infer<typeof ExperienceMemoryItemSchema>,
ctx?: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
try {
this.ensureWritable(ctx?.agentId);
const result = await userMemoryService.addExperienceMemory(params);
if (!result.success) {
return {
error: {
message: result.message,
type: 'PluginServerError',
},
success: false,
};
}
return {
content: `Experience memory "${params.title}" saved with memoryId: "${result.memoryId}" and experienceId: "${result.experienceId}"`,
state: { experienceId: result.experienceId, memoryId: result.memoryId },
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `addExperienceMemory with error detail: ${err.message}`,
error: {
body: error,
message: err.message,
type: 'PluginServerError',
},
success: false,
};
}
this.ensureWritable(ctx?.agentId);
return this.runtime.addExperienceMemory(params);
};
/**
* Add an identity memory
*/
addIdentityMemory = async (
params: z.infer<typeof AddIdentityActionSchema>,
ctx?: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
try {
this.ensureWritable(ctx?.agentId);
const result = await userMemoryService.addIdentityMemory(params);
if (!result.success) {
return {
error: {
message: result.message,
type: 'PluginServerError',
},
success: false,
};
}
return {
content: `Identity memory "${params.title}" saved with memoryId: "${result.memoryId}" and identityId: "${result.identityId}"`,
state: { identityId: result.identityId, memoryId: result.memoryId },
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `addIdentityMemory with error detail: ${err.message}`,
error: {
body: error,
message: err.message,
type: 'PluginServerError',
},
success: false,
};
}
this.ensureWritable(ctx?.agentId);
return this.runtime.addIdentityMemory(params);
};
/**
* Add a preference memory
*/
addPreferenceMemory = async (
params: z.infer<typeof PreferenceMemoryItemSchema>,
ctx?: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
try {
this.ensureWritable(ctx?.agentId);
const result = await userMemoryService.addPreferenceMemory(params);
if (!result.success) {
return {
error: {
message: result.message,
type: 'PluginServerError',
},
success: false,
};
}
return {
content: `Preference memory "${params.title}" saved with memoryId: "${result.memoryId}" and preferenceId: "${result.preferenceId}"`,
state: { memoryId: result.memoryId, preferenceId: result.preferenceId },
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `addPreferenceMemory with error detail: ${err.message}`,
error: {
body: error,
message: err.message,
type: 'PluginServerError',
},
success: false,
};
}
this.ensureWritable(ctx?.agentId);
return this.runtime.addPreferenceMemory(params);
};
// ==================== Update/Remove APIs ====================
/**
* Update an identity memory
*/
updateIdentityMemory = async (
params: z.infer<typeof UpdateIdentityActionSchema>,
ctx?: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
try {
this.ensureWritable(ctx?.agentId);
const result = await userMemoryService.updateIdentityMemory(params);
if (!result.success) {
return {
error: {
message: result.message,
type: 'PluginServerError',
},
success: false,
};
}
return {
content: `✏️ Identity memory updated: ${params.id}`,
state: { identityId: params.id },
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `updateIdentityMemory with error detail: ${err.message}`,
error: {
body: error,
message: err.message,
type: 'PluginServerError',
},
success: false,
};
}
this.ensureWritable(ctx?.agentId);
return this.runtime.updateIdentityMemory(params);
};
/**
* Remove an identity memory
*/
removeIdentityMemory = async (
params: z.infer<typeof RemoveIdentityActionSchema>,
ctx?: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
try {
this.ensureWritable(ctx?.agentId);
const result = await userMemoryService.removeIdentityMemory(params);
if (!result.success) {
return {
error: { message: result.message, type: 'PluginServerError' },
success: false,
};
}
return {
content: `🗑️ Identity memory removed: ${params.id}\nReason: ${params.reason}`,
state: { identityId: params.id, reason: params.reason },
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `removeIdentityMemory with error detail: ${err.message}`,
error: {
body: error,
message: err.message,
type: 'PluginServerError',
},
success: false,
};
}
this.ensureWritable(ctx?.agentId);
return this.runtime.removeIdentityMemory(params);
};
}
// Export the executor instance for registration
export const memoryExecutor = new MemoryExecutor();

View file

@ -0,0 +1,15 @@
{
"name": "@lobechat/builtin-tool-tools",
"version": "1.0.0",
"private": true,
"exports": {
".": "./src/index.ts",
"./executor": "./src/executor/index.ts",
"./executionRuntime": "./src/ExecutionRuntime/index.ts"
},
"main": "./src/index.ts",
"devDependencies": {
"@lobechat/prompts": "workspace:*",
"@lobechat/types": "workspace:*"
}
}

View file

@ -0,0 +1,112 @@
import type { BuiltinServerRuntimeOutput } from '@lobechat/types';
import type { ActivatedToolInfo, ActivateToolsParams } from '../types';
export interface ToolManifestInfo {
apiDescriptions: Array<{ description: string; name: string }>;
identifier: string;
name: string;
systemRole?: string;
}
export interface ToolsActivatorRuntimeService {
getActivatedToolIds: () => string[];
getToolManifests: (identifiers: string[]) => Promise<ToolManifestInfo[]>;
markActivated: (identifiers: string[]) => void;
}
export interface ToolsActivatorExecutionRuntimeOptions {
service: ToolsActivatorRuntimeService;
}
export class ToolsActivatorExecutionRuntime {
private service: ToolsActivatorRuntimeService;
constructor(options: ToolsActivatorExecutionRuntimeOptions) {
this.service = options.service;
}
async activateTools(args: ActivateToolsParams): Promise<BuiltinServerRuntimeOutput> {
const { identifiers } = args;
if (!identifiers || identifiers.length === 0) {
return {
content: 'No tool identifiers provided. Please specify which tools to activate.',
success: false,
};
}
try {
const alreadyActive = this.service.getActivatedToolIds();
const toActivate: string[] = [];
const alreadyActiveList: string[] = [];
for (const id of identifiers) {
if (alreadyActive.includes(id)) {
alreadyActiveList.push(id);
} else {
toActivate.push(id);
}
}
// Fetch manifests for tools to activate
const manifests = await this.service.getToolManifests(toActivate);
const foundIdentifiers = new Set(manifests.map((m) => m.identifier));
const notFound = toActivate.filter((id) => !foundIdentifiers.has(id));
const activatedTools: ActivatedToolInfo[] = manifests.map((m) => ({
apiCount: m.apiDescriptions.length,
identifier: m.identifier,
name: m.name,
}));
// Mark newly activated tools
if (manifests.length > 0) {
this.service.markActivated(manifests.map((m) => m.identifier));
}
// Build response content
const parts: string[] = [];
if (activatedTools.length > 0) {
parts.push('Successfully activated tools:');
for (const manifest of manifests) {
parts.push(`\n## ${manifest.name} (${manifest.identifier})`);
if (manifest.systemRole) {
parts.push(manifest.systemRole);
}
if (manifest.apiDescriptions.length > 0) {
parts.push('\nAvailable APIs:');
for (const api of manifest.apiDescriptions) {
parts.push(`- **${api.name}**: ${api.description}`);
}
}
}
}
if (alreadyActiveList.length > 0) {
parts.push(`\nAlready active: ${alreadyActiveList.join(', ')}`);
}
if (notFound.length > 0) {
parts.push(`\nNot found: ${notFound.join(', ')}`);
}
return {
content: parts.join('\n'),
state: {
activatedTools,
alreadyActive: alreadyActiveList,
notFound,
},
success: true,
};
} catch (e) {
return {
content: `Failed to activate tools: ${(e as Error).message}`,
success: false,
};
}
}
}

View file

@ -0,0 +1,47 @@
import { BaseExecutor, type BuiltinToolContext, type BuiltinToolResult } from '@lobechat/types';
import type { ToolsActivatorExecutionRuntime } from '../ExecutionRuntime';
import { type ActivateToolsParams, LobeToolIdentifier, ToolsActivatorApiName } from '../types';
class ToolsActivatorExecutor extends BaseExecutor<typeof ToolsActivatorApiName> {
readonly identifier = LobeToolIdentifier;
protected readonly apiEnum = ToolsActivatorApiName;
private runtime: ToolsActivatorExecutionRuntime;
constructor(runtime: ToolsActivatorExecutionRuntime) {
super();
this.runtime = runtime;
}
activateTools = async (
params: ActivateToolsParams,
ctx: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
try {
if (ctx.signal?.aborted) {
return { stop: true, success: false };
}
const result = await this.runtime.activateTools(params);
if (result.success) {
return { content: result.content, state: result.state, success: true };
}
return {
content: result.content,
error: { message: result.content, type: 'PluginServerError' },
success: false,
};
} catch (e) {
const err = e as Error;
return {
error: { body: e, message: err.message, type: 'PluginServerError' },
success: false,
};
}
};
}
export { ToolsActivatorExecutor };

View file

@ -0,0 +1,9 @@
export { LobeToolsManifest } from './manifest';
export { systemPrompt } from './systemRole';
export {
type ActivatedToolInfo,
type ActivateToolsParams,
type ActivateToolsState,
LobeToolIdentifier,
ToolsActivatorApiName,
} from './types';

View file

@ -0,0 +1,36 @@
import type { BuiltinToolManifest } from '@lobechat/types';
import { systemPrompt } from './systemRole';
import { LobeToolIdentifier, ToolsActivatorApiName } from './types';
export const LobeToolsManifest: BuiltinToolManifest = {
api: [
{
description:
'Activate tools from the <available_tools> list so their full API schemas become available for use. Call this before using any tool that is not yet activated. You can activate multiple tools at once.',
name: ToolsActivatorApiName.activateTools,
parameters: {
properties: {
identifiers: {
description:
'Array of tool identifiers to activate. Use the identifiers from the <available_tools> list.',
items: {
type: 'string',
},
type: 'array',
},
},
required: ['identifiers'],
type: 'object',
},
},
],
identifier: LobeToolIdentifier,
meta: {
avatar: '🔧',
description: 'Discover and activate tools on demand',
title: 'Tools',
},
systemRole: systemPrompt,
type: 'builtin',
};

View file

@ -0,0 +1,26 @@
export const systemPrompt = `You have access to a Tool Discovery system that allows you to dynamically activate tools on demand. Not all tools are loaded by default — you must activate them before use.
<how_it_works>
1. Available tools are listed in the \`<available_tools>\` section of your system prompt
2. Each entry shows the tool's identifier, name, and description
3. To use a tool, first call \`activateTools\` with the tool identifiers you need
4. After activation, the tool's full API schemas become available as native function calls in subsequent turns
5. You can activate multiple tools at once by passing multiple identifiers
</how_it_works>
<tool_selection_guidelines>
- **activateTools**: Call this when you need to use a tool that isn't yet activated
- Review the \`<available_tools>\` list to find relevant tools for the user's task
- Provide an array of tool identifiers to activate
- After activation, the tools' APIs will be available for you to call directly
- Tools that are already active will be noted in the response
- If an identifier is not found, it will be reported in the response
</tool_selection_guidelines>
<best_practices>
- Check the \`<available_tools>\` list before activating tools
- Activate all tools you'll need for a task in a single call when possible
- Only activate tools that are relevant to the user's current request
- After activation, use the tools' APIs directly no need to call activateTools again for the same tools
</best_practices>
`;

View file

@ -0,0 +1,21 @@
export const LobeToolIdentifier = 'lobe-tools';
export const ToolsActivatorApiName = {
activateTools: 'activateTools',
};
export interface ActivateToolsParams {
identifiers: string[];
}
export interface ActivatedToolInfo {
apiCount: number;
identifier: string;
name: string;
}
export interface ActivateToolsState {
activatedTools: ActivatedToolInfo[];
alreadyActive: string[];
notFound: string[];
}

View file

@ -0,0 +1,44 @@
{
"name": "@lobechat/builtin-tools",
"version": "1.0.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts",
"./renders": "./src/renders.ts",
"./inspectors": "./src/inspectors.ts",
"./interventions": "./src/interventions.ts",
"./placeholders": "./src/placeholders.ts",
"./portals": "./src/portals.ts",
"./streamings": "./src/streamings.ts",
"./identifiers": "./src/identifiers.ts",
"./dynamicInterventionAudits": "./src/dynamicInterventionAudits.ts"
},
"main": "./src/index.ts",
"dependencies": {
"@lobechat/builtin-tool-agent-builder": "workspace:*",
"@lobechat/builtin-tool-cloud-sandbox": "workspace:*",
"@lobechat/builtin-tool-group-agent-builder": "workspace:*",
"@lobechat/builtin-tool-group-management": "workspace:*",
"@lobechat/builtin-tool-gtd": "workspace:*",
"@lobechat/builtin-tool-knowledge-base": "workspace:*",
"@lobechat/builtin-tool-local-system": "workspace:*",
"@lobechat/builtin-tool-memory": "workspace:*",
"@lobechat/builtin-tool-notebook": "workspace:*",
"@lobechat/builtin-tool-page-agent": "workspace:*",
"@lobechat/builtin-tool-skills": "workspace:*",
"@lobechat/builtin-tool-tools": "workspace:*",
"@lobechat/builtin-tool-web-browsing": "workspace:*",
"@lobechat/const": "workspace:*"
},
"devDependencies": {
"@lobechat/types": "workspace:*"
},
"peerDependencies": {
"@lobehub/ui": "^4",
"antd-style": "*",
"lucide-react": "*",
"react": "*",
"react-i18next": "*"
}
}

View file

@ -9,6 +9,7 @@ import { MemoryManifest } from '@lobechat/builtin-tool-memory';
import { NotebookManifest } from '@lobechat/builtin-tool-notebook';
import { PageAgentManifest } from '@lobechat/builtin-tool-page-agent';
import { SkillsManifest } from '@lobechat/builtin-tool-skills';
import { LobeToolsManifest } from '@lobechat/builtin-tool-tools';
import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
export const builtinToolIdentifiers: string[] = [
@ -24,4 +25,5 @@ export const builtinToolIdentifiers: string[] = [
GTDManifest.identifier,
MemoryManifest.identifier,
NotebookManifest.identifier,
LobeToolsManifest.identifier,
];

View file

@ -9,6 +9,7 @@ import { MemoryManifest } from '@lobechat/builtin-tool-memory';
import { NotebookManifest } from '@lobechat/builtin-tool-notebook';
import { PageAgentManifest } from '@lobechat/builtin-tool-page-agent';
import { SkillsManifest } from '@lobechat/builtin-tool-skills';
import { LobeToolsManifest } from '@lobechat/builtin-tool-tools';
import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
import { isDesktop } from '@lobechat/const';
import { type LobeBuiltinTool } from '@lobechat/types';
@ -92,4 +93,10 @@ export const builtinTools: LobeBuiltinTool[] = [
manifest: NotebookManifest,
type: 'builtin',
},
{
hidden: true,
identifier: LobeToolsManifest.identifier,
manifest: LobeToolsManifest,
type: 'builtin',
},
];

View file

@ -1,6 +1,7 @@
import { builtinTools } from '@lobechat/builtin-tools';
import {
KLAVIS_SERVER_TYPES,
getLobehubSkillProviderById,
KLAVIS_SERVER_TYPES,
type KlavisServerType,
type LobehubSkillProviderType,
} from '@lobechat/const';
@ -13,23 +14,22 @@ import { Link } from 'react-router-dom';
import urlJoin from 'url-join';
import { useDiscoverStore } from '@/store/discover';
import { builtinTools } from '@/tools';
/**
* Icon component for built-in tools (Klavis & LobehubSkill)
* For string type icon, use Image component to render
* For IconType type icon, use Icon component to render with theme fill color
*/
const BuiltinToolIcon = memo<
Pick<KlavisServerType | LobehubSkillProviderType, 'icon' | 'label'>
>(({ icon, label }) => {
if (typeof icon === 'string') {
return <Image alt={label} height={40} src={icon} style={{ flex: 'none' }} width={40} />;
}
const BuiltinToolIcon = memo<Pick<KlavisServerType | LobehubSkillProviderType, 'icon' | 'label'>>(
({ icon, label }) => {
if (typeof icon === 'string') {
return <Image alt={label} height={40} src={icon} style={{ flex: 'none' }} width={40} />;
}
// Use theme color fill, automatically adapts in dark mode
return <Icon fill={cssVar.colorText} icon={icon} size={40} />;
});
// Use theme color fill, automatically adapts in dark mode
return <Icon fill={cssVar.colorText} icon={icon} size={40} />;
},
);
BuiltinToolIcon.displayName = 'BuiltinToolIcon';
@ -195,7 +195,7 @@ const PluginItem = memo<PluginItemProps>(({ identifier }) => {
if (isLoading)
return (
<Block gap={12} horizontal key={identifier} padding={12} variant={'outlined'}>
<Block horizontal gap={12} key={identifier} padding={12} variant={'outlined'}>
<Skeleton paragraph={{ rows: 1 }} title={false} />
</Block>
);
@ -216,9 +216,9 @@ const PluginItem = memo<PluginItemProps>(({ identifier }) => {
const content = (
<Block
horizontal
className={cx(sourceConfig.clickable ? styles.clickable : styles.noLink)}
gap={12}
horizontal
key={identifier}
padding={12}
variant={'outlined'}
@ -232,7 +232,7 @@ const PluginItem = memo<PluginItemProps>(({ identifier }) => {
}}
>
<div className={styles.titleRow}>
<Text as={'h2'} className={cx(styles.title, 'plugin-title')} ellipsis>
<Text ellipsis as={'h2'} className={cx(styles.title, 'plugin-title')}>
{data.title}
</Text>
{sourceConfig.tagText && (

View file

@ -1,10 +1,10 @@
import { getBuiltinIntervention } from '@lobechat/builtin-tools/interventions';
import { safeParseJSON } from '@lobechat/utils';
import { Flexbox } from '@lobehub/ui';
import { memo, Suspense, useCallback, useMemo, useRef, useState } from 'react';
import { useUserStore } from '@/store/user';
import { toolInterventionSelectors } from '@/store/user/selectors';
import { getBuiltinIntervention } from '@/tools/interventions';
import { useConversationStore } from '../../../../../store';
import Arguments from '../Arguments';

View file

@ -1,9 +1,8 @@
import { getBuiltinPlaceholder } from '@lobechat/builtin-tools/placeholders';
import { getBuiltinStreaming } from '@lobechat/builtin-tools/streamings';
import { safeParseJSON } from '@lobechat/utils';
import { memo } from 'react';
import { getBuiltinPlaceholder } from '@/tools/placeholders';
import { getBuiltinStreaming } from '@/tools/streamings';
import Arguments from '../Arguments';
interface LoadingPlaceholderProps {

View file

@ -1,8 +1,7 @@
import { getBuiltinRender } from '@lobechat/builtin-tools/renders';
import { type ChatPluginPayload } from '@lobechat/types';
import { memo } from 'react';
import { getBuiltinRender } from '@/tools/renders';
import CustomRender from './CustomRender';
import { FallbackArgumentRender } from './FallbacktArgumentRender';

View file

@ -1,10 +1,9 @@
import { getBuiltinStreaming } from '@lobechat/builtin-tools/streamings';
import { type ChatToolResult, type ToolIntervention } from '@lobechat/types';
import { safeParsePartialJSON } from '@lobechat/utils';
import { Flexbox } from '@lobehub/ui';
import { memo, Suspense } from 'react';
import { getBuiltinStreaming } from '@/tools/streamings';
import AbortResponse from './AbortResponse';
import ErrorResponse from './ErrorResponse';
import Intervention from './Intervention';

View file

@ -1,3 +1,4 @@
import { builtinToolIdentifiers } from '@lobechat/builtin-tools/identifiers';
import { Icon } from '@lobehub/ui';
import { createStaticStyles, cx } from 'antd-style';
import isEqual from 'fast-deep-equal';
@ -8,7 +9,6 @@ import { useTranslation } from 'react-i18next';
import { pluginHelpers, useToolStore } from '@/store/tool';
import { toolSelectors } from '@/store/tool/selectors';
import { shinyTextStyles } from '@/styles';
import { builtinToolIdentifiers } from '@/tools/identifiers';
export const styles = createStaticStyles(({ css, cssVar }) => ({
aborted: css`

View file

@ -1,10 +1,10 @@
import { getBuiltinInspector } from '@lobechat/builtin-tools/inspectors';
import { type ToolIntervention } from '@lobechat/types';
import { safeParseJSON, safeParsePartialJSON } from '@lobechat/utils';
import { Flexbox } from '@lobehub/ui';
import { memo } from 'react';
import { LOADING_FLAT } from '@/const/message';
import { getBuiltinInspector } from '@/tools/inspectors';
import StatusIndicator from './StatusIndicator';
import ToolTitle from './ToolTitle';

View file

@ -1,3 +1,5 @@
import { getBuiltinRender } from '@lobechat/builtin-tools/renders';
import { getBuiltinStreaming } from '@lobechat/builtin-tools/streamings';
import { LOADING_FLAT } from '@lobechat/const';
import { type ChatToolResult, type ToolIntervention } from '@lobechat/types';
import { AccordionItem, Flexbox, Skeleton } from '@lobehub/ui';
@ -9,8 +11,6 @@ import { useChatStore } from '@/store/chat';
import { operationSelectors } from '@/store/chat/slices/operation/selectors';
import { useToolStore } from '@/store/tool';
import { toolSelectors } from '@/store/tool/selectors';
import { getBuiltinRender } from '@/tools/renders';
import { getBuiltinStreaming } from '@/tools/streamings';
import { ToolErrorBoundary } from '../../Tool/ErrorBoundary';
import Actions from './Actions';

View file

@ -1,10 +1,10 @@
import { getBuiltinRender } from '@lobechat/builtin-tools/renders';
import { Accordion, AccordionItem, Flexbox, Skeleton } from '@lobehub/ui';
import { type CSSProperties } from 'react';
import { memo, useState } from 'react';
import Actions from '@/features/Conversation/Messages/AssistantGroup/Tool/Actions';
import dynamic from '@/libs/next/dynamic';
import { getBuiltinRender } from '@/tools/renders';
import { dataSelectors, messageStateSelectors, useConversationStore } from '../../../store';
import Inspectors from '../../AssistantGroup/Tool/Inspector';

View file

@ -9,7 +9,7 @@ const mockCodeInterpreterRender = vi.fn(({ content }) => (
<div>CodeInterpreterRender: {content}</div>
));
vi.mock('@/tools/renders', () => ({
vi.mock('@lobechat/builtin-tools/renders', () => ({
getBuiltinRender: vi.fn((identifier, apiName) => {
if (identifier === 'lobe-web-browsing') return mockWebBrowsingRender;
if (identifier === 'lobe-code-interpreter') return mockCodeInterpreterRender;

View file

@ -1,8 +1,7 @@
import { getBuiltinRender } from '@lobechat/builtin-tools/renders';
import { safeParseJSON } from '@lobechat/utils';
import { memo } from 'react';
import { getBuiltinRender } from '@/tools/renders';
import { useParseContent } from '../useParseContent';
export interface BuiltinTypeProps {

View file

@ -1,10 +1,10 @@
import { BuiltinToolsPortals } from '@lobechat/builtin-tools/portals';
import isEqual from 'fast-deep-equal';
import { memo } from 'react';
import PluginRender from '@/features/PluginsUI/Render';
import { useChatStore } from '@/store/chat';
import { chatPortalSelectors, dbMessageSelectors } from '@/store/chat/selectors';
import { BuiltinToolsPortals } from '@/tools/portals';
import { safeParseJSON } from '@/utils/safeParseJSON';
const ToolRender = memo(() => {

View file

@ -3,6 +3,7 @@
*/
import { KnowledgeBaseManifest } from '@lobechat/builtin-tool-knowledge-base';
import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
import { defaultToolIds } from '@lobechat/builtin-tools';
import { isDesktop } from '@lobechat/const';
import { type PluginEnableChecker } from '@lobechat/context-engine';
import { ToolsEngine } from '@lobechat/context-engine';
@ -17,7 +18,6 @@ import {
lobehubSkillStoreSelectors,
pluginSelectors,
} from '@/store/tool/selectors';
import { defaultToolIds } from '@/tools';
import { getSearchConfig } from '../getSearchConfig';
import { isCanUseFC } from '../isCanUseFC';
@ -135,8 +135,8 @@ export const getEnabledTools = (
return (
toolsEngine.generateTools({
model: model, // Use provided model or fallback
provider: provider, // Use provided provider or fallback
model, // Use provided model or fallback
provider, // Use provided provider or fallback
toolIds,
}) || []
);

View file

@ -560,6 +560,7 @@ export const createRuntimeExecutors = (
// Execute tool using ToolExecutionService
log(`[${operationLogId}] Executing tool ${toolName} ...`);
const executionResult = await toolExecutionService.executeTool(chatToolPayload, {
memoryToolPermission: agentConfig?.chatConfig?.memory?.toolPermission,
serverDB: ctx.serverDB,
toolManifestMap: state.toolManifestMap,
toolResultMaxLength,
@ -736,6 +737,7 @@ export const createRuntimeExecutors = (
try {
log(`[${operationLogId}] Executing tool ${toolName} ...`);
const executionResult = await toolExecutionService.executeTool(chatToolPayload, {
memoryToolPermission: state.metadata?.agentConfig?.chatConfig?.memory?.toolPermission,
serverDB: ctx.serverDB,
toolManifestMap: state.toolManifestMap,
topicId: ctx.topicId,

View file

@ -2,11 +2,10 @@
import { KnowledgeBaseManifest } from '@lobechat/builtin-tool-knowledge-base';
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
import { builtinTools } from '@lobechat/builtin-tools';
import { ToolsEngine } from '@lobechat/context-engine';
import { describe, expect, it } from 'vitest';
import { builtinTools } from '@/tools';
import { createServerAgentToolsEngine, createServerToolsEngine } from '../index';
import { type InstalledPlugin, type ServerAgentToolsContext } from '../types';

View file

@ -12,12 +12,11 @@
import { KnowledgeBaseManifest } from '@lobechat/builtin-tool-knowledge-base';
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
import { builtinTools, defaultToolIds } from '@lobechat/builtin-tools';
import { type LobeToolManifest } from '@lobechat/context-engine';
import { ToolsEngine } from '@lobechat/context-engine';
import debug from 'debug';
import { builtinTools, defaultToolIds } from '@/tools';
import {
type ServerAgentToolsContext,
type ServerAgentToolsEngineConfig,

View file

@ -1,7 +1,7 @@
import { type AgentRuntimeContext, type AgentState } from '@lobechat/agent-runtime';
import type { AgentRuntimeContext, AgentState } from '@lobechat/agent-runtime';
import { AgentRuntime, GeneralChatAgent } from '@lobechat/agent-runtime';
import { type ChatMessageError } from '@lobechat/types';
import { AgentRuntimeErrorType, ChatErrorType } from '@lobechat/types';
import { dynamicInterventionAudits } from '@lobechat/builtin-tools/dynamicInterventionAudits';
import { AgentRuntimeErrorType, ChatErrorType, type ChatMessageError } from '@lobechat/types';
import debug from 'debug';
import urlJoin from 'url-join';
@ -19,7 +19,6 @@ import { QueueService } from '@/server/services/queue';
import { LocalQueueServiceImpl } from '@/server/services/queue/impls';
import { ToolExecutionService } from '@/server/services/toolExecution';
import { BuiltinToolsExecutor } from '@/server/services/toolExecution/builtin';
import { dynamicInterventionAudits } from '@/tools/dynamicInterventionAudits';
import {
type AgentExecutionParams,

View file

@ -47,7 +47,7 @@ vi.mock('@/server/services/toolExecution', () => ({
vi.mock('@/server/services/toolExecution/builtin', () => ({
BuiltinToolsExecutor: vi.fn().mockImplementation(() => ({})),
}));
vi.mock('@/tools/dynamicInterventionAudits', () => ({
vi.mock('@lobechat/builtin-tools/dynamicInterventionAudits', () => ({
dynamicInterventionAudits: [],
}));

View file

@ -1130,7 +1130,7 @@ export class DiscoverService {
}
// Step 3: Try to find in builtin tools
const { builtinTools } = await import('@/tools/index');
const { builtinTools } = await import('@lobechat/builtin-tools');
const builtinTool = builtinTools.find((tool) => tool.identifier === identifier);
if (builtinTool) {
log('getPluginDetail: found builtin tool for identifier=%s', identifier);
@ -1800,7 +1800,15 @@ export class DiscoverService {
return undefined;
}
const { user, agents, agentGroups, forkedAgents, forkedAgentGroups, favoriteAgents, favoriteAgentGroups } = response;
const {
user,
agents,
agentGroups,
forkedAgents,
forkedAgentGroups,
favoriteAgents,
favoriteAgentGroups,
} = response;
// Transform agents to DiscoverAssistantItem format
const transformedAgents: DiscoverAssistantItem[] = (agents || []).map((agent: any) => ({

View file

@ -8,8 +8,10 @@
*/
import { type ToolExecutionContext } from '../types';
import { cloudSandboxRuntime } from './cloudSandbox';
import { memoryRuntime } from './memory';
import { notebookRuntime } from './notebook';
import { skillsRuntime } from './skills';
import { toolsActivatorRuntime } from './tools';
import { type ServerRuntimeFactory, type ServerRuntimeRegistration } from './types';
import { webBrowsingRuntime } from './webBrowsing';
@ -28,7 +30,14 @@ const registerRuntimes = (runtimes: ServerRuntimeRegistration[]) => {
};
// Register all server runtimes
registerRuntimes([webBrowsingRuntime, cloudSandboxRuntime, notebookRuntime, skillsRuntime]);
registerRuntimes([
webBrowsingRuntime,
cloudSandboxRuntime,
notebookRuntime,
skillsRuntime,
memoryRuntime,
toolsActivatorRuntime,
]);
// ==================== Registry API ====================

View file

@ -0,0 +1,698 @@
import { MemoryIdentifier } from '@lobechat/builtin-tool-memory';
import {
MemoryExecutionRuntime,
type MemoryRuntimeService,
} from '@lobechat/builtin-tool-memory/executionRuntime';
import { BRANDING_PROVIDER, ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
import {
DEFAULT_USER_MEMORY_EMBEDDING_DIMENSIONS,
DEFAULT_USER_MEMORY_EMBEDDING_MODEL_ITEM,
MEMORY_SEARCH_TOP_K_LIMITS,
} from '@lobechat/const';
import type { LobeChatDatabase } from '@lobechat/database';
import type {
ActivityMemoryItemSchema,
AddIdentityActionSchema,
ContextMemoryItemSchema,
ExperienceMemoryItemSchema,
PreferenceMemoryItemSchema,
RemoveIdentityActionSchema,
UpdateIdentityActionSchema,
} from '@lobechat/memory-user-memory/schemas';
import type {
AddActivityMemoryResult,
AddContextMemoryResult,
AddExperienceMemoryResult,
AddIdentityMemoryResult,
AddPreferenceMemoryResult,
RemoveIdentityMemoryResult,
SearchMemoryParams,
SearchMemoryResult,
UpdateIdentityMemoryResult,
} from '@lobechat/types';
import { LayersEnum } from '@lobechat/types';
import { eq } from 'drizzle-orm';
import type { z } from 'zod';
import {
type IdentityEntryBasePayload,
type IdentityEntryPayload,
UserMemoryModel,
} from '@/database/models/userMemory';
import { userSettings } from '@/database/schemas';
import { getServerDefaultFilesConfig } from '@/server/globalConfig';
import { initModelRuntimeFromDB } from '@/server/modules/ModelRuntime';
import type { ServerRuntimeRegistration } from './types';
type MemoryEffort = 'high' | 'low' | 'medium';
const normalizeMemoryEffort = (value: unknown): MemoryEffort => {
if (value === 'low' || value === 'medium' || value === 'high') return value;
return 'medium';
};
const applySearchLimitsByEffort = (
effort: MemoryEffort,
requested: { activities: number; contexts: number; experiences: number; preferences: number },
) => {
const limit = MEMORY_SEARCH_TOP_K_LIMITS[effort];
return {
activities: Math.min(requested.activities, limit.activities),
contexts: Math.min(requested.contexts, limit.contexts),
experiences: Math.min(requested.experiences, limit.experiences),
preferences: Math.min(requested.preferences, limit.preferences),
};
};
const mapMemorySearchResult = (
layeredResults: Awaited<ReturnType<UserMemoryModel['searchWithEmbedding']>>,
): SearchMemoryResult => {
return {
activities: layeredResults.activities.map((activity) => ({
accessedAt: activity.accessedAt,
associatedLocations: activity.associatedLocations,
associatedObjects: activity.associatedObjects,
associatedSubjects: activity.associatedSubjects,
capturedAt: activity.capturedAt,
createdAt: activity.createdAt,
endsAt: activity.endsAt,
feedback: activity.feedback,
id: activity.id,
metadata: activity.metadata,
narrative: activity.narrative,
notes: activity.notes,
startsAt: activity.startsAt,
status: activity.status,
tags: activity.tags,
timezone: activity.timezone,
type: activity.type,
updatedAt: activity.updatedAt,
userMemoryId: activity.userMemoryId,
})),
contexts: layeredResults.contexts.map((context) => ({
accessedAt: context.accessedAt,
associatedObjects: context.associatedObjects,
associatedSubjects: context.associatedSubjects,
createdAt: context.createdAt,
currentStatus: context.currentStatus,
description: context.description,
id: context.id,
metadata: context.metadata,
scoreImpact: context.scoreImpact,
scoreUrgency: context.scoreUrgency,
tags: context.tags,
title: context.title,
type: context.type,
updatedAt: context.updatedAt,
userMemoryIds: Array.isArray(context.userMemoryIds)
? (context.userMemoryIds as string[])
: null,
})),
experiences: layeredResults.experiences.map((experience) => ({
accessedAt: experience.accessedAt,
action: experience.action,
createdAt: experience.createdAt,
id: experience.id,
keyLearning: experience.keyLearning,
metadata: experience.metadata,
possibleOutcome: experience.possibleOutcome,
reasoning: experience.reasoning,
scoreConfidence: experience.scoreConfidence,
situation: experience.situation,
tags: experience.tags,
type: experience.type,
updatedAt: experience.updatedAt,
userMemoryId: experience.userMemoryId,
})),
preferences: layeredResults.preferences.map((preference) => ({
accessedAt: preference.accessedAt,
conclusionDirectives: preference.conclusionDirectives,
createdAt: preference.createdAt,
id: preference.id,
metadata: preference.metadata,
scorePriority: preference.scorePriority,
suggestions: preference.suggestions,
tags: preference.tags,
type: preference.type,
updatedAt: preference.updatedAt,
userMemoryId: preference.userMemoryId,
})),
} satisfies SearchMemoryResult;
};
const getEmbeddingRuntime = async (serverDB: LobeChatDatabase, userId: string) => {
const { provider, model: embeddingModel } =
getServerDefaultFilesConfig().embeddingModel || DEFAULT_USER_MEMORY_EMBEDDING_MODEL_ITEM;
const agentRuntime = await initModelRuntimeFromDB(
serverDB,
userId,
ENABLE_BUSINESS_FEATURES ? BRANDING_PROVIDER : provider,
);
return { agentRuntime, embeddingModel };
};
const createEmbedder = (agentRuntime: any, embeddingModel: string) => {
return async (value?: string | null): Promise<number[] | undefined> => {
if (!value || value.trim().length === 0) return undefined;
const embeddings = await agentRuntime.embeddings({
dimensions: DEFAULT_USER_MEMORY_EMBEDDING_DIMENSIONS,
input: value,
model: embeddingModel,
});
return embeddings?.[0];
};
};
class MemoryServerRuntimeService implements MemoryRuntimeService {
private memoryModel: UserMemoryModel;
private serverDB: LobeChatDatabase;
private userId: string;
private memoryEffort: MemoryEffort;
constructor(options: {
memoryEffort: MemoryEffort;
memoryModel: UserMemoryModel;
serverDB: LobeChatDatabase;
userId: string;
}) {
this.memoryModel = options.memoryModel;
this.serverDB = options.serverDB;
this.userId = options.userId;
this.memoryEffort = options.memoryEffort;
}
searchMemory = async (params: SearchMemoryParams): Promise<SearchMemoryResult> => {
const { provider, model: embeddingModel } =
getServerDefaultFilesConfig().embeddingModel || DEFAULT_USER_MEMORY_EMBEDDING_MODEL_ITEM;
const modelRuntime = await initModelRuntimeFromDB(this.serverDB, this.userId, provider);
const queryEmbeddings = await modelRuntime.embeddings({
dimensions: DEFAULT_USER_MEMORY_EMBEDDING_DIMENSIONS,
input: params.query,
model: embeddingModel,
});
const effectiveEffort = normalizeMemoryEffort(params.effort ?? this.memoryEffort);
const effortDefaults = MEMORY_SEARCH_TOP_K_LIMITS[effectiveEffort];
const requestedLimits = {
activities: params.topK?.activities ?? effortDefaults.activities,
contexts: params.topK?.contexts ?? effortDefaults.contexts,
experiences: params.topK?.experiences ?? effortDefaults.experiences,
preferences: params.topK?.preferences ?? effortDefaults.preferences,
};
const effortConstrainedLimits = applySearchLimitsByEffort(effectiveEffort, requestedLimits);
const layeredResults = await this.memoryModel.searchWithEmbedding({
embedding: queryEmbeddings?.[0],
limits: effortConstrainedLimits,
});
return mapMemorySearchResult(layeredResults);
};
addContextMemory = async (
input: z.infer<typeof ContextMemoryItemSchema>,
): Promise<AddContextMemoryResult> => {
try {
const { agentRuntime, embeddingModel } = await getEmbeddingRuntime(
this.serverDB,
this.userId,
);
const embed = createEmbedder(agentRuntime, embeddingModel);
const summaryEmbedding = await embed(input.summary);
const detailsEmbedding = await embed(input.details);
const contextDescriptionEmbedding = await embed(input.withContext.description);
const { context, memory } = await this.memoryModel.createContextMemory({
context: {
associatedObjects:
UserMemoryModel.parseAssociatedObjects(input.withContext.associatedObjects) ?? null,
associatedSubjects:
UserMemoryModel.parseAssociatedSubjects(input.withContext.associatedSubjects) ?? null,
currentStatus: input.withContext.currentStatus ?? null,
description: input.withContext.description ?? null,
descriptionVector: contextDescriptionEmbedding ?? null,
metadata: {},
scoreImpact: input.withContext.scoreImpact ?? null,
scoreUrgency: input.withContext.scoreUrgency ?? null,
tags: input.tags ?? [],
title: input.withContext.title ?? null,
type: input.withContext.type ?? null,
},
details: input.details || '',
detailsEmbedding,
memoryCategory: input.memoryCategory,
memoryLayer: LayersEnum.Context,
memoryType: input.memoryType,
summary: input.summary,
summaryEmbedding,
title: input.title,
});
return {
contextId: context.id,
memoryId: memory.id,
message: 'Memory saved successfully',
success: true,
};
} catch (error) {
return {
message: `Failed to save memory: ${(error as Error).message}`,
success: false,
};
}
};
addActivityMemory = async (
input: z.infer<typeof ActivityMemoryItemSchema>,
): Promise<AddActivityMemoryResult> => {
try {
const { agentRuntime, embeddingModel } = await getEmbeddingRuntime(
this.serverDB,
this.userId,
);
const embed = createEmbedder(agentRuntime, embeddingModel);
const summaryEmbedding = await embed(input.summary);
const detailsEmbedding = await embed(input.details);
const narrativeVector = await embed(input.withActivity.narrative);
const feedbackVector = await embed(input.withActivity.feedback);
const { activity, memory } = await this.memoryModel.createActivityMemory({
activity: {
associatedLocations:
UserMemoryModel.parseAssociatedLocations(input.withActivity.associatedLocations) ??
null,
associatedObjects:
UserMemoryModel.parseAssociatedObjects(input.withActivity.associatedObjects) ?? [],
associatedSubjects:
UserMemoryModel.parseAssociatedSubjects(input.withActivity.associatedSubjects) ?? [],
endsAt: UserMemoryModel.parseDateFromString(input.withActivity.endsAt ?? undefined),
feedback: input.withActivity.feedback ?? null,
feedbackVector: feedbackVector ?? null,
metadata: input.withActivity.metadata ?? null,
narrative: input.withActivity.narrative ?? null,
narrativeVector: narrativeVector ?? null,
notes: input.withActivity.notes ?? null,
startsAt: UserMemoryModel.parseDateFromString(input.withActivity.startsAt ?? undefined),
status: input.withActivity.status ?? 'pending',
tags: input.withActivity.tags ?? input.tags ?? [],
timezone: input.withActivity.timezone ?? null,
type: input.withActivity.type ?? 'other',
},
details: input.details || '',
detailsEmbedding,
memoryCategory: input.memoryCategory,
memoryLayer: LayersEnum.Activity,
memoryType: input.memoryType,
summary: input.summary,
summaryEmbedding,
title: input.title,
});
return {
activityId: activity.id,
memoryId: memory.id,
message: 'Memory saved successfully',
success: true,
};
} catch (error) {
return {
message: `Failed to save memory: ${(error as Error).message}`,
success: false,
};
}
};
addExperienceMemory = async (
input: z.infer<typeof ExperienceMemoryItemSchema>,
): Promise<AddExperienceMemoryResult> => {
try {
const { agentRuntime, embeddingModel } = await getEmbeddingRuntime(
this.serverDB,
this.userId,
);
const embed = createEmbedder(agentRuntime, embeddingModel);
const summaryEmbedding = await embed(input.summary);
const detailsEmbedding = await embed(input.details);
const situationVector = await embed(input.withExperience.situation);
const actionVector = await embed(input.withExperience.action);
const keyLearningVector = await embed(input.withExperience.keyLearning);
const { experience, memory } = await this.memoryModel.createExperienceMemory({
details: input.details || '',
detailsEmbedding,
experience: {
action: input.withExperience.action ?? null,
actionVector: actionVector ?? null,
keyLearning: input.withExperience.keyLearning ?? null,
keyLearningVector: keyLearningVector ?? null,
metadata: {},
possibleOutcome: input.withExperience.possibleOutcome ?? null,
reasoning: input.withExperience.reasoning ?? null,
scoreConfidence: input.withExperience.scoreConfidence ?? null,
situation: input.withExperience.situation ?? null,
situationVector: situationVector ?? null,
tags: input.tags ?? [],
type: input.memoryType,
},
memoryCategory: input.memoryCategory,
memoryLayer: LayersEnum.Experience,
memoryType: input.memoryType,
summary: input.summary,
summaryEmbedding,
title: input.title,
});
return {
experienceId: experience.id,
memoryId: memory.id,
message: 'Memory saved successfully',
success: true,
};
} catch (error) {
return {
message: `Failed to save memory: ${(error as Error).message}`,
success: false,
};
}
};
addIdentityMemory = async (
input: z.infer<typeof AddIdentityActionSchema>,
): Promise<AddIdentityMemoryResult> => {
try {
const { agentRuntime, embeddingModel } = await getEmbeddingRuntime(
this.serverDB,
this.userId,
);
const embed = createEmbedder(agentRuntime, embeddingModel);
const summaryEmbedding = await embed(input.summary);
const detailsEmbedding = await embed(input.details);
const descriptionEmbedding = await embed(input.withIdentity.description);
const identityMetadata: Record<string, unknown> = {};
if (
input.withIdentity.scoreConfidence !== null &&
input.withIdentity.scoreConfidence !== undefined
) {
identityMetadata.scoreConfidence = input.withIdentity.scoreConfidence;
}
if (
input.withIdentity.sourceEvidence !== null &&
input.withIdentity.sourceEvidence !== undefined
) {
identityMetadata.sourceEvidence = input.withIdentity.sourceEvidence;
}
const { identityId, userMemoryId } = await this.memoryModel.addIdentityEntry({
base: {
details: input.details,
detailsVector1024: detailsEmbedding ?? null,
memoryCategory: input.memoryCategory,
memoryLayer: LayersEnum.Identity,
memoryType: input.memoryType,
metadata: Object.keys(identityMetadata).length > 0 ? identityMetadata : undefined,
summary: input.summary,
summaryVector1024: summaryEmbedding ?? null,
tags: input.tags,
title: input.title,
},
identity: {
description: input.withIdentity.description,
descriptionVector: descriptionEmbedding ?? null,
episodicDate: input.withIdentity.episodicDate,
metadata: Object.keys(identityMetadata).length > 0 ? identityMetadata : undefined,
relationship: input.withIdentity.relationship,
role: input.withIdentity.role,
tags: input.tags,
type: input.withIdentity.type,
},
});
return {
identityId,
memoryId: userMemoryId,
message: 'Identity memory saved successfully',
success: true,
};
} catch (error) {
return {
message: `Failed to save identity memory: ${(error as Error).message}`,
success: false,
};
}
};
addPreferenceMemory = async (
input: z.infer<typeof PreferenceMemoryItemSchema>,
): Promise<AddPreferenceMemoryResult> => {
try {
const { agentRuntime, embeddingModel } = await getEmbeddingRuntime(
this.serverDB,
this.userId,
);
const embed = createEmbedder(agentRuntime, embeddingModel);
const summaryEmbedding = await embed(input.summary);
const detailsEmbedding = await embed(input.details);
const conclusionVector = await embed(input.withPreference.conclusionDirectives);
const suggestionsText =
input.withPreference?.suggestions?.length && input.withPreference?.suggestions?.length > 0
? input.withPreference?.suggestions?.join('\n')
: null;
const metadata = {
appContext: input.withPreference.appContext,
extractedScopes: input.withPreference.extractedScopes,
originContext: input.withPreference.originContext,
} satisfies Record<string, unknown>;
const { memory, preference } = await this.memoryModel.createPreferenceMemory({
details: input.details || '',
detailsEmbedding,
memoryCategory: input.memoryCategory,
memoryLayer: LayersEnum.Preference,
memoryType: input.memoryType,
preference: {
conclusionDirectives: input.withPreference.conclusionDirectives || '',
conclusionDirectivesVector: conclusionVector ?? null,
metadata,
scorePriority: input.withPreference.scorePriority ?? null,
suggestions: suggestionsText,
tags: input.tags,
type: input.memoryType,
},
summary: input.summary,
summaryEmbedding,
title: input.title,
});
return {
memoryId: memory.id,
message: 'Memory saved successfully',
preferenceId: preference.id,
success: true,
};
} catch (error) {
return {
message: `Failed to save memory: ${(error as Error).message}`,
success: false,
};
}
};
updateIdentityMemory = async (
input: z.infer<typeof UpdateIdentityActionSchema>,
): Promise<UpdateIdentityMemoryResult> => {
try {
const { agentRuntime, embeddingModel } = await getEmbeddingRuntime(
this.serverDB,
this.userId,
);
const embed = createEmbedder(agentRuntime, embeddingModel);
let summaryVector1024: number[] | null | undefined;
if (input.set.summary !== undefined) {
const vector = await embed(input.set.summary);
summaryVector1024 = vector ?? null;
}
let detailsVector1024: number[] | null | undefined;
if (input.set.details !== undefined) {
const vector = await embed(input.set.details);
detailsVector1024 = vector ?? null;
}
let descriptionVector: number[] | null | undefined;
if (input.set.withIdentity.description !== undefined) {
const vector = await embed(input.set.withIdentity.description);
descriptionVector = vector ?? null;
}
const metadataUpdates: Record<string, unknown> = {};
if (Object.hasOwn(input.set.withIdentity, 'scoreConfidence')) {
metadataUpdates.scoreConfidence = input.set.withIdentity.scoreConfidence ?? null;
}
if (Object.hasOwn(input.set.withIdentity, 'sourceEvidence')) {
metadataUpdates.sourceEvidence = input.set.withIdentity.sourceEvidence ?? null;
}
const identityPayload: Partial<IdentityEntryPayload> = {};
if (input.set.withIdentity.description !== undefined) {
identityPayload.description = input.set.withIdentity.description;
identityPayload.descriptionVector = descriptionVector;
}
if (input.set.withIdentity.episodicDate !== undefined) {
identityPayload.episodicDate = input.set.withIdentity.episodicDate;
}
if (input.set.withIdentity.relationship !== undefined) {
identityPayload.relationship = input.set.withIdentity.relationship;
}
if (input.set.withIdentity.role !== undefined) {
identityPayload.role = input.set.withIdentity.role;
}
if (input.set.tags !== undefined) {
identityPayload.tags = input.set.tags;
}
if (input.set.withIdentity.type !== undefined) {
identityPayload.type = input.set.withIdentity.type;
}
if (Object.keys(metadataUpdates).length > 0) {
identityPayload.metadata = metadataUpdates;
}
const basePayload: Partial<IdentityEntryBasePayload> = {};
if (input.set.details !== undefined) {
basePayload.details = input.set.details;
basePayload.detailsVector1024 = detailsVector1024;
}
if (input.set.memoryCategory !== undefined) {
basePayload.memoryCategory = input.set.memoryCategory;
}
if (input.set.memoryType !== undefined) {
basePayload.memoryType = input.set.memoryType;
}
if (input.set.summary !== undefined) {
basePayload.summary = input.set.summary;
basePayload.summaryVector1024 = summaryVector1024;
}
if (input.set.tags !== undefined) {
basePayload.tags = input.set.tags;
}
if (input.set.title !== undefined) {
basePayload.title = input.set.title;
}
if (Object.keys(metadataUpdates).length > 0) {
basePayload.metadata = metadataUpdates;
}
const updated = await this.memoryModel.updateIdentityEntry({
base: Object.keys(basePayload).length > 0 ? basePayload : undefined,
identity: Object.keys(identityPayload).length > 0 ? identityPayload : undefined,
identityId: input.id,
mergeStrategy: input.mergeStrategy,
});
if (!updated) {
return {
message: 'Identity memory not found',
success: false,
};
}
return {
identityId: input.id,
message: 'Identity memory updated successfully',
success: true,
};
} catch (error) {
return {
message: `Failed to update identity memory: ${(error as Error).message}`,
success: false,
};
}
};
removeIdentityMemory = async (
input: z.infer<typeof RemoveIdentityActionSchema>,
): Promise<RemoveIdentityMemoryResult> => {
try {
const removed = await this.memoryModel.removeIdentityEntry(input.id);
if (!removed) {
return {
message: 'Identity memory not found',
success: false,
};
}
return {
identityId: input.id,
message: 'Identity memory removed successfully',
reason: input.reason,
success: true,
};
} catch (error) {
return {
message: `Failed to remove identity memory: ${(error as Error).message}`,
success: false,
};
}
};
}
export const memoryRuntime: ServerRuntimeRegistration = {
factory: async (context) => {
if (!context.serverDB) {
throw new Error('serverDB is required for Memory execution');
}
if (!context.userId) {
throw new Error('userId is required for Memory execution');
}
// Resolve memoryEffort from user settings
let memoryEffort: MemoryEffort = 'medium';
try {
const userSettingsRow = await context.serverDB.query.userSettings.findFirst({
columns: { memory: true },
where: eq(userSettings.id, context.userId),
});
const memoryConfig =
typeof userSettingsRow?.memory === 'object' && userSettingsRow?.memory !== null
? (userSettingsRow.memory as { effort?: unknown })
: undefined;
memoryEffort = normalizeMemoryEffort(memoryConfig?.effort);
} catch {
// fallback to medium
}
const memoryModel = new UserMemoryModel(context.serverDB, context.userId);
const service = new MemoryServerRuntimeService({
memoryEffort,
memoryModel,
serverDB: context.serverDB,
userId: context.userId,
});
return new MemoryExecutionRuntime({
service,
toolPermission: context.memoryToolPermission,
});
},
identifier: MemoryIdentifier,
};

View file

@ -0,0 +1,37 @@
import { LobeToolIdentifier } from '@lobechat/builtin-tool-tools';
import {
type ToolManifestInfo,
ToolsActivatorExecutionRuntime,
type ToolsActivatorRuntimeService,
} from '@lobechat/builtin-tool-tools/executionRuntime';
import { type ServerRuntimeRegistration } from './types';
/**
* Tools Activator Server Runtime
* Stub implementation actual tool manifest resolution
* will be implemented in follow-up work when ToolDiscoveryProvider is ready.
*/
export const toolsActivatorRuntime: ServerRuntimeRegistration = {
factory: (_context) => {
const activatedIds: string[] = [];
const service: ToolsActivatorRuntimeService = {
getActivatedToolIds: () => [...activatedIds],
getToolManifests: async (_identifiers: string[]): Promise<ToolManifestInfo[]> => {
// Stub: will be replaced with real tool manifest lookup
return [];
},
markActivated: (identifiers: string[]) => {
for (const id of identifiers) {
if (!activatedIds.includes(id)) {
activatedIds.push(id);
}
}
},
};
return new ToolsActivatorExecutionRuntime({ service });
},
identifier: LobeToolIdentifier,
};

View file

@ -3,6 +3,8 @@ import { type LobeChatDatabase } from '@lobechat/database';
import { type ChatToolPayload } from '@lobechat/types';
export interface ToolExecutionContext {
/** Memory tool permission from agent chat config */
memoryToolPermission?: 'read-only' | 'read-write';
/** Server database for LobeHub Skills execution */
serverDB?: LobeChatDatabase;
toolManifestMap: Record<string, LobeToolManifest>;

View file

@ -7,6 +7,7 @@ import {
} from '@lobechat/agent-runtime';
import { AgentRuntime, computeStepContext, GeneralChatAgent } from '@lobechat/agent-runtime';
import { PageAgentIdentifier } from '@lobechat/builtin-tool-page-agent';
import { dynamicInterventionAudits } from '@lobechat/builtin-tools/dynamicInterventionAudits';
import { isDesktop } from '@lobechat/const';
import {
type ChatToolPayload,
@ -36,7 +37,6 @@ import { pageAgentRuntime } from '@/store/tool/slices/builtin/executors/lobe-pag
import { type StoreSetter } from '@/store/types';
import { toolInterventionSelectors } from '@/store/user/selectors';
import { getUserStoreState } from '@/store/user/store';
import { dynamicInterventionAudits } from '@/tools/dynamicInterventionAudits';
import { markdownToTxt } from '@/utils/markdownToTxt';
import { topicSelectors } from '../../../selectors';

View file

@ -1,4 +1,5 @@
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
import { builtinTools } from '@lobechat/builtin-tools';
import { ToolArgumentsRepairer, ToolNameResolver } from '@lobechat/context-engine';
import { type ChatToolPayload, type MessageToolCall } from '@lobechat/types';
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
@ -11,7 +12,6 @@ import {
pluginSelectors,
} from '@/store/tool/selectors';
import { type StoreSetter } from '@/store/types';
import { builtinTools } from '@/tools';
/**
* Internal utility methods and runtime state management

View file

@ -17,6 +17,7 @@ import type { BuiltinToolContext, BuiltinToolResult, IBuiltinToolExecutor } from
import { notebookExecutor } from './lobe-notebook';
import { pageAgentExecutor } from './lobe-page-agent';
import { skillsExecutor } from './lobe-skills';
import { toolsActivatorExecutor } from './lobe-tools';
import { webBrowsing } from './lobe-web-browsing';
// ==================== Import and register all executors ====================
@ -132,5 +133,6 @@ registerExecutors([
notebookExecutor,
pageAgentExecutor,
skillsExecutor,
toolsActivatorExecutor,
webBrowsing,
]);

View file

@ -0,0 +1,31 @@
/**
* Lobe Tools Executor
*
* Creates and exports the ToolsActivatorExecutor instance for registration.
* Injects a stub service as dependency actual tool manifest resolution
* will be implemented in follow-up work when ToolDiscoveryProvider is ready.
*/
import {
type ToolManifestInfo,
ToolsActivatorExecutionRuntime,
type ToolsActivatorRuntimeService,
} from '@lobechat/builtin-tool-tools/executionRuntime';
import { ToolsActivatorExecutor } from '@lobechat/builtin-tool-tools/executor';
// Stub service — will be replaced with real implementation
// when ToolDiscoveryProvider and state.tools mutations are ready
const stubService: ToolsActivatorRuntimeService = {
getActivatedToolIds: () => [],
getToolManifests: async (_identifiers: string[]): Promise<ToolManifestInfo[]> => {
return [];
},
markActivated: () => {},
};
// Create runtime with stub service
const runtime = new ToolsActivatorExecutionRuntime({
service: stubService,
});
// Create executor instance with the runtime
export const toolsActivatorExecutor = new ToolsActivatorExecutor(runtime);

View file

@ -1,8 +1,7 @@
import { builtinSkills } from '@lobechat/builtin-skills';
import { builtinTools } from '@lobechat/builtin-tools';
import { type BuiltinSkill, type LobeBuiltinTool } from '@lobechat/types';
import { builtinTools } from '@/tools';
export interface BuiltinToolState {
builtinSkills: BuiltinSkill[];
builtinToolLoading: Record<string, boolean>;