🐛 fix: fix group sub task execution (#11595)

* update memory manifest

* support console error in the server with subagent task

*  test(agent-service): add unit tests for getAgentConfig method

Add 7 test cases for the new getAgentConfig(idOrSlug) method:
- Return null if agent does not exist
- Support lookup by agent id
- Support lookup by slug
- Merge DEFAULT_AGENT_CONFIG and serverDefaultAgentConfig
- Use default model/provider when agent has none
- Prioritize agent model/provider over defaults
- Merge user default agent config

Relates to: LOBE-3514

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

*  feat(group-agent-builder): add GetAgentInfo Inspector

Add Inspector component for getAgentInfo API to display agent avatar
and name in the tool call UI.

Changes:
- Add GetAgentInfoInspector component with avatar and title display
- Register inspector in GroupAgentBuilderInspectors registry
- Add i18n translations for en-US and zh-CN

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix lobehub manifest temporarily

* fix twitter calling

* 🔧 chore: remove unused serializeError function

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* 🐛 fix(test): fix execAgent.threadId test mock for AgentService

Add AgentService mock and use importOriginal for model-bank mock
to fix test failures after refactoring to use AgentService.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Arvin Xu
2026-01-19 03:16:02 +08:00
committed by GitHub
parent ef27ed0824
commit 32be2b2882
24 changed files with 1255 additions and 61 deletions

View File

@@ -40,6 +40,7 @@
"builtins.lobe-cloud-sandbox.title": "Cloud Sandbox",
"builtins.lobe-group-agent-builder.apiName.batchCreateAgents": "Batch create agents",
"builtins.lobe-group-agent-builder.apiName.createAgent": "Create agent",
"builtins.lobe-group-agent-builder.apiName.getAgentInfo": "Get member info",
"builtins.lobe-group-agent-builder.apiName.getAvailableModels": "Get available models",
"builtins.lobe-group-agent-builder.apiName.installPlugin": "Install Skill",
"builtins.lobe-group-agent-builder.apiName.inviteAgent": "Invite member",

View File

@@ -40,6 +40,7 @@
"builtins.lobe-cloud-sandbox.title": "云端沙盒",
"builtins.lobe-group-agent-builder.apiName.batchCreateAgents": "批量创建 Agent",
"builtins.lobe-group-agent-builder.apiName.createAgent": "创建助理",
"builtins.lobe-group-agent-builder.apiName.getAgentInfo": "获取成员信息",
"builtins.lobe-group-agent-builder.apiName.getAvailableModels": "获取可用模型",
"builtins.lobe-group-agent-builder.apiName.installPlugin": "安装技能",
"builtins.lobe-group-agent-builder.apiName.inviteAgent": "邀请成员",

View File

@@ -17,6 +17,8 @@ export interface AgentState {
tools?: any[];
systemRole?: string;
toolManifestMap: Record<string, any>;
/** Tool source map for routing tool execution to correct handler */
toolSourceMap?: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis' | 'lobehubSkill'>;
/**
* Model runtime configuration

View File

@@ -0,0 +1,68 @@
'use client';
import type { BuiltinInspectorProps } from '@lobechat/types';
import { Avatar, Flexbox } from '@lobehub/ui';
import { createStaticStyles, cx } from 'antd-style';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { shinyTextStyles } from '@/styles';
import type { GetAgentInfoParams } from '../../../types';
interface GetAgentInfoState {
avatar?: string;
title?: string;
}
const styles = createStaticStyles(({ css, cssVar: cv }) => ({
root: css`
overflow: hidden;
display: flex;
gap: 8px;
align-items: center;
`,
title: css`
flex-shrink: 0;
color: ${cv.colorTextSecondary};
white-space: nowrap;
`,
}));
export const GetAgentInfoInspector = memo<
BuiltinInspectorProps<GetAgentInfoParams, GetAgentInfoState>
>(({ args, partialArgs, isArgumentsStreaming, isLoading, pluginState }) => {
const { t } = useTranslation('plugin');
const agentId = args?.agentId || partialArgs?.agentId;
const title = pluginState?.title;
const avatar = pluginState?.avatar;
// Initial streaming state
if (isArgumentsStreaming && !agentId) {
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-group-agent-builder.apiName.getAgentInfo')}</span>
</div>
);
}
return (
<Flexbox
align={'center'}
className={cx(styles.root, (isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText)}
gap={8}
horizontal
>
<span className={styles.title}>
{t('builtins.lobe-group-agent-builder.apiName.getAgentInfo')}:
</span>
{avatar && <Avatar avatar={avatar} shape={'square'} size={20} title={title || undefined} />}
<span>{title || agentId}</span>
</Flexbox>
);
});
GetAgentInfoInspector.displayName = 'GetAgentInfoInspector';
export default GetAgentInfoInspector;

View File

@@ -10,6 +10,7 @@ import { type BuiltinInspector } from '@lobechat/types';
import { GroupAgentBuilderApiName } from '../../types';
import { BatchCreateAgentsInspector } from './BatchCreateAgents';
import { CreateAgentInspector } from './CreateAgent';
import { GetAgentInfoInspector } from './GetAgentInfo';
import { InviteAgentInspector } from './InviteAgent';
import { RemoveAgentInspector } from './RemoveAgent';
import { SearchAgentInspector } from './SearchAgent';
@@ -27,6 +28,7 @@ export const GroupAgentBuilderInspectors: Record<string, BuiltinInspector> = {
// Group-specific inspectors
[GroupAgentBuilderApiName.batchCreateAgents]: BatchCreateAgentsInspector as BuiltinInspector,
[GroupAgentBuilderApiName.createAgent]: CreateAgentInspector as BuiltinInspector,
[GroupAgentBuilderApiName.getAgentInfo]: GetAgentInfoInspector as BuiltinInspector,
[GroupAgentBuilderApiName.inviteAgent]: InviteAgentInspector as BuiltinInspector,
[GroupAgentBuilderApiName.removeAgent]: RemoveAgentInspector as BuiltinInspector,
[GroupAgentBuilderApiName.searchAgent]: SearchAgentInspector as BuiltinInspector,
@@ -44,6 +46,7 @@ export const GroupAgentBuilderInspectors: Record<string, BuiltinInspector> = {
// Re-export individual inspectors
export { BatchCreateAgentsInspector } from './BatchCreateAgents';
export { CreateAgentInspector } from './CreateAgent';
export { GetAgentInfoInspector } from './GetAgentInfo';
export { InviteAgentInspector } from './InviteAgent';
export { RemoveAgentInspector } from './RemoveAgent';
export { SearchAgentInspector } from './SearchAgent';

View File

@@ -1,13 +1,13 @@
import {
addIdentityJsonSchema,
contextMemoryJsonSchema,
experienceMemoryJsonSchema,
preferenceMemoryJsonSchema,
removeIdentityJsonSchema,
searchMemoryJsonSchema,
updateIdentityJsonSchema,
} from '@lobechat/memory-user-memory/schemas';
import type { BuiltinToolManifest } from '@lobechat/types';
import {
CONTEXT_OBJECT_TYPES,
CONTEXT_STATUS,
CONTEXT_SUBJECT_TYPES,
IDENTITY_TYPES,
MEMORY_TYPES,
MERGE_STRATEGIES,
RELATIONSHIPS,
} from '@lobechat/types';
import { systemPrompt } from './systemRole';
import { MemoryApiName } from './types';
@@ -20,43 +20,608 @@ export const MemoryManifest: BuiltinToolManifest = {
description:
'Retrieve memories based on a search query. Use this to recall previously saved information.',
name: MemoryApiName.searchUserMemory,
parameters: searchMemoryJsonSchema,
parameters: {
additionalProperties: false,
properties: {
query: { type: 'string' },
topK: {
additionalProperties: false,
properties: {
contexts: { minimum: 0, type: 'integer' },
experiences: { minimum: 0, type: 'integer' },
preferences: { minimum: 0, type: 'integer' },
},
required: ['contexts', 'experiences', 'preferences'],
type: 'object',
},
},
required: ['query', 'topK'],
type: 'object',
},
},
{
description:
'Create a context memory that captures ongoing situations, projects, or environments. Include actors, resources, statuses, urgency/impact, and a clear description.',
name: MemoryApiName.addContextMemory,
parameters: contextMemoryJsonSchema,
parameters: {
additionalProperties: false,
properties: {
details: {
description: 'Optional detailed information',
type: 'string',
},
memoryCategory: {
description: 'Memory category',
type: 'string',
},
memoryType: {
description: 'Memory type',
enum: MEMORY_TYPES,
type: 'string',
},
summary: {
description: 'Concise overview of this specific memory',
type: 'string',
},
tags: {
description: 'User defined tags that summarize the context facets',
items: { type: 'string' },
type: 'array',
},
title: {
description: 'Brief descriptive title',
type: 'string',
},
withContext: {
additionalProperties: false,
properties: {
associatedObjects: {
description:
'Array of objects describing involved roles, entities, or resources, [] empty if none',
items: {
additionalProperties: false,
properties: {
extra: {
description:
'Additional metadata about the object, should always be a valid JSON string if present',
type: ['string', 'null'],
},
name: {
description: 'Name of the associated object',
type: 'string',
},
type: {
description: 'Type/category of the associated object',
enum: CONTEXT_OBJECT_TYPES,
type: 'string',
},
},
required: ['extra', 'name', 'type'],
type: 'object',
},
type: 'array',
},
associatedSubjects: {
description:
'Array of JSON objects describing involved subjects or participants, [] empty if none',
items: {
additionalProperties: false,
properties: {
extra: {
description:
'Additional metadata about the subject, should always be a valid JSON string if present',
type: ['string', 'null'],
},
name: {
description: 'Name of the associated subject',
type: 'string',
},
type: {
description: 'Type/category of the associated subject',
enum: CONTEXT_SUBJECT_TYPES,
type: 'string',
},
},
required: ['extra', 'name', 'type'],
type: 'object',
},
type: 'array',
},
currentStatus: {
description:
"High level status markers (must be one of 'planned', 'ongoing', 'completed', 'aborted', 'on_hold', 'cancelled')",
enum: CONTEXT_STATUS,
type: 'string',
},
description: {
description: 'Rich narrative describing the situation, timeline, or environment',
type: 'string',
},
labels: {
description: 'Model generated tags that summarize the context themes',
items: { type: 'string' },
type: 'array',
},
scoreImpact: {
description: 'Numeric score (0-1 (0% to 100%)) describing importance',
maximum: 1,
minimum: 0,
type: 'number',
},
scoreUrgency: {
description: 'Numeric score (0-1 (0% to 100%)) describing urgency',
maximum: 1,
minimum: 0,
type: 'number',
},
title: {
description: 'Optional synthesized context headline',
type: 'string',
},
type: {
description:
"High level context archetype (e.g., 'project', 'relationship', 'goal')",
type: 'string',
},
},
required: [
'associatedObjects',
'associatedSubjects',
'currentStatus',
'description',
'labels',
'scoreImpact',
'scoreUrgency',
'title',
'type',
],
type: 'object',
},
},
required: [
'details',
'memoryCategory',
'memoryType',
'summary',
'tags',
'title',
'withContext',
],
type: 'object',
},
},
{
description:
'Record an experience memory capturing situation, actions, reasoning, outcomes, and confidence. Use for lessons, playbooks, or transferable know-how.',
name: MemoryApiName.addExperienceMemory,
parameters: experienceMemoryJsonSchema,
parameters: {
additionalProperties: false,
properties: {
details: {
description: 'Optional detailed information',
type: 'string',
},
memoryCategory: {
description: 'Memory category',
type: 'string',
},
memoryType: {
description: 'Memory type',
enum: MEMORY_TYPES,
type: 'string',
},
summary: {
description: 'Concise overview of this specific memory',
type: 'string',
},
tags: {
description: 'Model generated tags that summarize the experience facets',
items: { type: 'string' },
type: 'array',
},
title: {
description: 'Brief descriptive title',
type: 'string',
},
withExperience: {
additionalProperties: false,
properties: {
action: {
description: 'Narrative describing actions taken or behaviors exhibited',
type: 'string',
},
keyLearning: {
description: 'Narrative describing key insights or lessons learned',
type: 'string',
},
knowledgeValueScore: {
description:
'Numeric score (0-1) describing how reusable and shareable this experience is',
maximum: 1,
minimum: 0,
type: 'number',
},
labels: {
description: 'Model generated tags that summarize the experience facets',
items: { type: 'string' },
type: 'array',
},
possibleOutcome: {
description: 'Narrative describing potential outcomes or learnings',
type: 'string',
},
problemSolvingScore: {
description:
'Numeric score (0-1) describing how effectively the problem was solved',
maximum: 1,
minimum: 0,
type: 'number',
},
reasoning: {
description: 'Narrative describing the thought process or motivations',
type: 'string',
},
scoreConfidence: {
description:
'Numeric score (0-1 (0% to 100%)) describing confidence in the experience details',
maximum: 1,
minimum: 0,
type: 'number',
},
situation: {
description: 'Narrative describing the situation or event',
type: 'string',
},
type: {
description: 'Type of experience being recorded',
type: 'string',
},
},
required: [
'action',
'keyLearning',
'knowledgeValueScore',
'labels',
'possibleOutcome',
'problemSolvingScore',
'reasoning',
'scoreConfidence',
'situation',
'type',
],
type: 'object',
},
},
required: [
'details',
'memoryCategory',
'memoryType',
'summary',
'tags',
'title',
'withExperience',
],
type: 'object',
},
},
{
description:
'Add an identity memory describing enduring facts about a person, their role, relationship, and supporting evidence. Use to track self/others identities.',
name: MemoryApiName.addIdentityMemory,
parameters: addIdentityJsonSchema,
parameters: {
additionalProperties: false,
properties: {
details: {
description: 'Optional detailed information',
type: ['string', 'null'],
},
memoryCategory: {
description: 'Memory category',
type: 'string',
},
memoryType: {
description: 'Memory type',
enum: MEMORY_TYPES,
type: 'string',
},
summary: {
description: 'Concise overview of this specific memory',
type: 'string',
},
tags: {
description: 'Model generated tags that summarize the identity facets',
items: { type: 'string' },
type: 'array',
},
title: {
description:
'Honorific-style, concise descriptor (strength + domain/milestone), avoid bare job titles; e.g., "Trusted open-source maintainer", "Specializes in low-latency infra", "Former Aliyun engineer", "Cares for rescue cats"',
type: 'string',
},
withIdentity: {
additionalProperties: false,
properties: {
description: { type: 'string' },
episodicDate: { type: ['string', 'null'] },
extractedLabels: {
items: { type: 'string' },
type: 'array',
},
relationship: {
enum: RELATIONSHIPS,
type: 'string',
},
role: {
description:
'Role explicitly mentioned for this identity entry (e.g., "platform engineer", "caregiver"); keep neutral and only use when evidence exists',
type: 'string',
},
scoreConfidence: { type: 'number' },
sourceEvidence: { type: ['string', 'null'] },
type: {
enum: IDENTITY_TYPES,
type: 'string',
},
},
required: [
'description',
'episodicDate',
'extractedLabels',
'relationship',
'role',
'scoreConfidence',
'sourceEvidence',
'type',
],
type: 'object',
},
},
required: [
'details',
'memoryCategory',
'memoryType',
'summary',
'tags',
'title',
'withIdentity',
],
type: 'object',
},
},
{
description:
'Create a preference memory that encodes durable directives or choices the assistant should follow. Include conclusionDirectives, scopes, and context.',
name: MemoryApiName.addPreferenceMemory,
parameters: preferenceMemoryJsonSchema,
parameters: {
additionalProperties: false,
properties: {
details: {
description: 'Optional detailed information',
type: 'string',
},
memoryCategory: {
description: 'Memory category',
type: 'string',
},
memoryType: {
description: 'Memory type',
enum: MEMORY_TYPES,
type: 'string',
},
summary: {
description: 'Concise overview of this specific memory',
type: 'string',
},
tags: {
description: 'Model generated tags that summarize the preference facets',
items: { type: 'string' },
type: 'array',
},
title: {
description: 'Brief descriptive title',
type: 'string',
},
withPreference: {
additionalProperties: false,
properties: {
appContext: {
additionalProperties: false,
description: 'Application/surface specific preference, if any',
properties: {
app: {
description: 'App or product name this applies to',
type: ['string', 'null'],
},
feature: { type: ['string', 'null'] },
route: { type: ['string', 'null'] },
surface: {
description: 'e.g., chat, emails, code review, notes',
type: ['string', 'null'],
},
},
required: ['app', 'feature', 'route', 'surface'],
type: ['object', 'null'],
},
conclusionDirectives: {
description:
"Direct, self-contained instruction to the assistant from the user's perspective (what to do, not how to implement)",
type: 'string',
},
extractedLabels: {
description: 'Model generated tags that summarize the preference facets',
items: { type: 'string' },
type: 'array',
},
extractedScopes: {
description:
'Array of JSON strings describing preference facets and applicable scopes',
items: { type: 'string' },
type: 'array',
},
originContext: {
additionalProperties: false,
description: 'Context of how/why this preference was expressed',
properties: {
actor: {
description: "Who stated the preference; use 'User' for the user",
type: 'string',
},
applicableWhen: {
description: 'Conditions where this preference applies',
type: ['string', 'null'],
},
notApplicableWhen: {
description: 'Conditions where it does not apply',
type: ['string', 'null'],
},
scenario: {
description: 'Applicable scenario or use case',
type: ['string', 'null'],
},
trigger: {
description: 'What prompted this preference',
type: ['string', 'null'],
},
},
required: ['actor', 'applicableWhen', 'notApplicableWhen', 'scenario', 'trigger'],
type: ['object', 'null'],
},
scorePriority: {
description:
'Numeric prioritization weight (0-1 (0% to 100%)) where higher means more critical to respect',
maximum: 1,
minimum: 0,
type: 'number',
},
suggestions: {
description: 'Follow-up actions or assistant guidance derived from the preference',
items: { type: 'string' },
type: 'array',
},
type: {
description:
"High level preference classification (e.g., 'lifestyle', 'communication')",
type: 'string',
},
},
required: [
'appContext',
'conclusionDirectives',
'extractedLabels',
'extractedScopes',
'originContext',
'scorePriority',
'suggestions',
'type',
],
type: 'object',
},
},
required: [
'title',
'summary',
'tags',
'details',
'memoryCategory',
'memoryType',
'withPreference',
],
type: 'object',
},
},
{
description:
'Update an existing identity memory with refined details, relationships, roles, or tags. Use mergeStrategy to control replacement vs merge.',
name: MemoryApiName.updateIdentityMemory,
parameters: updateIdentityJsonSchema,
parameters: {
additionalProperties: false,
properties: {
id: { type: 'string' },
mergeStrategy: {
enum: MERGE_STRATEGIES,
type: 'string',
},
set: {
additionalProperties: false,
properties: {
details: {
description: 'Optional detailed information, use null for omitting the field',
type: ['string', 'null'],
},
memoryCategory: {
description: 'Memory category, use null for omitting the field',
type: ['string', 'null'],
},
memoryType: {
description: 'Memory type, use null for omitting the field',
enum: [...MEMORY_TYPES, null],
},
summary: {
description:
'Concise overview of this specific memory, use null for omitting the field',
type: ['string', 'null'],
},
tags: {
description:
'Model generated tags that summarize the identity facets, use null for omitting the field',
items: { type: 'string' },
type: ['array', 'null'],
},
title: {
description:
'Honorific-style, concise descriptor (strength + domain/milestone), avoid bare job titles; e.g., "Trusted open-source maintainer", "Specializes in low-latency infra", "Former Aliyun engineer", "Cares for rescue cats"; use null for omitting the field',
type: ['string', 'null'],
},
withIdentity: {
additionalProperties: false,
properties: {
description: { type: ['string', 'null'] },
episodicDate: { type: ['string', 'null'] },
extractedLabels: {
items: { type: 'string' },
type: ['array', 'null'],
},
relationship: {
description: `Possible values: ${RELATIONSHIPS.join(' | ')}`,
type: ['string', 'null'],
},
role: {
description:
'Role explicitly mentioned for this identity entry (e.g., "platform engineer", "caregiver"); keep existing when not updated; use null for omitting the field',
type: ['string', 'null'],
},
scoreConfidence: { type: ['number', 'null'] },
sourceEvidence: { type: ['string', 'null'] },
type: {
description: `Possible values: ${IDENTITY_TYPES.join(' | ')}`,
type: ['string', 'null'],
},
},
required: ['description', 'extractedLabels', 'role'],
type: 'object',
},
},
required: ['withIdentity'],
type: 'object',
},
},
required: ['id', 'mergeStrategy', 'set'],
type: 'object',
},
},
{
description:
'Remove an identity memory when it is incorrect, obsolete, or duplicated. Always provide a concise reason.',
name: MemoryApiName.removeIdentityMemory,
parameters: removeIdentityJsonSchema,
parameters: {
additionalProperties: false,
properties: {
id: { type: 'string' },
reason: { type: 'string' },
},
required: ['id', 'reason'],
type: 'object',
},
},
],
identifier: 'lobe-user-memory',
@@ -67,6 +632,3 @@ export const MemoryManifest: BuiltinToolManifest = {
systemRole: systemPrompt,
type: 'builtin',
};
/** @deprecated Use MemoryManifest instead */
export const UserMemoryManifest = MemoryManifest;

View File

@@ -27,8 +27,8 @@ export interface ThreadMetadata {
completedAt?: string;
/** Execution duration in milliseconds */
duration?: number;
/** Error message when task failed */
error?: string;
/** Error details when task failed */
error?: any;
/** Operation ID for tracking */
operationId?: string;
/** Task start time, used to calculate duration */

View File

@@ -10,19 +10,33 @@ export enum UserMemoryContextObjectType {
Knowledge = 'knowledge',
Other = 'other',
Person = 'person',
Place = 'place'
Place = 'place',
}
export const CONTEXT_OBJECT_TYPES = Object.values(UserMemoryContextObjectType);
export enum UserMemoryContextSubjectType {
Item = 'item',
Other = 'other',
Person = 'person',
Pet = 'pet'
Pet = 'pet',
}
export const CONTEXT_SUBJECT_TYPES = Object.values(UserMemoryContextSubjectType);
export interface UserMemoryContext extends UserMemoryTimestamps {
associatedObjects: { extra?: Record<string, unknown> | null, name?: string, type?: UserMemoryContextObjectType }[] | null;
associatedSubjects: { extra?: Record<string, unknown> | null, name?: string, type?: UserMemoryContextSubjectType }[] | null;
associatedObjects:
| {
extra?: Record<string, unknown> | null;
name?: string;
type?: UserMemoryContextObjectType;
}[]
| null;
associatedSubjects:
| {
extra?: Record<string, unknown> | null;
name?: string;
type?: UserMemoryContextSubjectType;
}[]
| null;
currentStatus: string | null;
description: string | null;
descriptionVector: number[] | null;
@@ -97,7 +111,4 @@ export type UserMemoryPreferenceWithoutVectors = Omit<
'conclusionDirectivesVector'
>;
export type UserMemoryPreferencesListItem = Omit<
UserMemoryPreferenceWithoutVectors,
'suggestions'
>;
export type UserMemoryPreferencesListItem = Omit<UserMemoryPreferenceWithoutVectors, 'suggestions'>;

View File

@@ -30,17 +30,20 @@ export enum RelationshipEnum {
Uncle = 'uncle',
Wife = 'wife',
}
export const RELATIONSHIPS = Object.values(RelationshipEnum);
export enum MergeStrategyEnum {
Merge = 'merge',
Replace = 'replace',
}
export const MERGE_STRATEGIES = Object.values(MergeStrategyEnum);
export enum IdentityTypeEnum {
Demographic = 'demographic',
Personal = 'personal',
Professional = 'professional',
}
export const IDENTITY_TYPES = Object.values(IdentityTypeEnum);
export enum LayersEnum {
Context = 'context',
@@ -48,6 +51,7 @@ export enum LayersEnum {
Identity = 'identity',
Preference = 'preference',
}
export const MEMORY_LAYERS = Object.values(LayersEnum);
export enum TypesEnum {
Activity = 'activity',
@@ -61,6 +65,7 @@ export enum TypesEnum {
Technology = 'technology',
Topic = 'topic',
}
export const MEMORY_TYPES = Object.values(TypesEnum);
export enum ContextStatusEnum {
Aborted = 'aborted',
@@ -68,5 +73,6 @@ export enum ContextStatusEnum {
Completed = 'completed',
OnHold = 'on_hold',
Ongoing = 'ongoing',
Planned = 'planned'
Planned = 'planned',
}
export const CONTEXT_STATUS = Object.values(ContextStatusEnum);

View File

@@ -40,6 +40,7 @@ export default {
'builtins.lobe-cloud-sandbox.title': 'Cloud Sandbox',
'builtins.lobe-group-agent-builder.apiName.batchCreateAgents': 'Batch create agents',
'builtins.lobe-group-agent-builder.apiName.createAgent': 'Create agent',
'builtins.lobe-group-agent-builder.apiName.getAgentInfo': 'Get member info',
'builtins.lobe-group-agent-builder.apiName.getAvailableModels': 'Get available models',
'builtins.lobe-group-agent-builder.apiName.installPlugin': 'Install Skill',
'builtins.lobe-group-agent-builder.apiName.inviteAgent': 'Invite member',

View File

@@ -55,6 +55,8 @@ export const createRuntimeExecutors = (
// Fallback to state's modelRuntimeConfig if not in payload
const model = llmPayload.model || state.modelRuntimeConfig?.model;
const provider = llmPayload.provider || state.modelRuntimeConfig?.provider;
// Fallback to state's tools if not in payload
const tools = llmPayload.tools || state.tools;
if (!model || !provider) {
throw new Error('Model and provider are required for call_llm instruction');
@@ -128,14 +130,14 @@ export const createRuntimeExecutors = (
const chatPayload = {
messages: llmPayload.messages,
model,
tools: llmPayload.tools,
tools,
};
log(
`${stagePrefix} calling model-runtime chat (model: %s, messages: %d, tools: %d)`,
model,
llmPayload.messages.length,
llmPayload.tools?.length ?? 0,
tools?.length ?? 0,
);
// Buffer: accumulate text and reasoning, send every 50ms
@@ -261,7 +263,12 @@ export const createRuntimeExecutors = (
}
},
onToolsCalling: async ({ toolsCalling: raw }) => {
const payload = new ToolNameResolver().resolve(raw, state.toolManifestMap);
const resolved = new ToolNameResolver().resolve(raw, state.toolManifestMap);
// Add source field from toolSourceMap for routing tool execution
const payload = resolved.map((p) => ({
...p,
source: state.toolSourceMap?.[p.identifier],
}));
// log(`[${operationLogId}][toolsCalling]`, payload);
toolsCalling = payload;
tool_calls = raw;
@@ -466,6 +473,7 @@ export const createRuntimeExecutors = (
// Execute tool using ToolExecutionService
log(`[${operationLogId}] Executing tool ${toolName} ...`);
const executionResult = await toolExecutionService.executeTool(chatToolPayload, {
serverDB: ctx.serverDB,
toolManifestMap: state.toolManifestMap,
userId: ctx.userId,
});

View File

@@ -12,8 +12,7 @@
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 { ToolsEngine } from '@lobechat/context-engine';
import type { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
import { type LobeToolManifest, ToolsEngine } from '@lobechat/context-engine';
import debug from 'debug';
import { builtinTools } from '@/tools';
@@ -50,11 +49,11 @@ export const createServerToolsEngine = (
// Get plugin manifests from installed plugins (from database)
const pluginManifests = context.installedPlugins
.map((plugin) => plugin.manifest as LobeChatPluginManifest)
.map((plugin) => plugin.manifest as LobeToolManifest)
.filter(Boolean);
// Get all builtin tool manifests
const builtinManifests = builtinTools.map((tool) => tool.manifest as LobeChatPluginManifest);
const builtinManifests = builtinTools.map((tool) => tool.manifest as LobeToolManifest);
// Combine all manifests
const allManifests = [...pluginManifests, ...builtinManifests, ...additionalManifests];
@@ -87,18 +86,27 @@ export const createServerAgentToolsEngine = (
context: ServerAgentToolsContext,
params: ServerCreateAgentToolsEngineParams,
): ToolsEngine => {
const { agentConfig, model, provider, hasEnabledKnowledgeBases = false } = params;
const {
additionalManifests,
agentConfig,
hasEnabledKnowledgeBases = false,
model,
provider,
} = params;
const searchMode = agentConfig.chatConfig?.searchMode ?? 'off';
const isSearchEnabled = searchMode !== 'off';
log(
'Creating agent tools engine for model=%s, provider=%s, searchMode=%s',
'Creating agent tools engine for model=%s, provider=%s, searchMode=%s, additionalManifests=%d',
model,
provider,
searchMode,
additionalManifests?.length ?? 0,
);
return createServerToolsEngine(context, {
// Pass additional manifests (e.g., LobeHub Skills)
additionalManifests,
// Add default tools based on configuration
defaultToolIds: [WebBrowsingManifest.identifier, KnowledgeBaseManifest.identifier],
// Create search-aware enableChecker for this request

View File

@@ -1,6 +1,5 @@
import type { PluginEnableChecker } from '@lobechat/context-engine';
import type { LobeToolManifest, PluginEnableChecker } from '@lobechat/context-engine';
import type { LobeTool } from '@lobechat/types';
import type { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
/**
* Installed plugin with manifest
@@ -22,7 +21,7 @@ export interface ServerAgentToolsContext {
*/
export interface ServerAgentToolsEngineConfig {
/** Additional manifests to include (e.g., Klavis tools) */
additionalManifests?: LobeChatPluginManifest[];
additionalManifests?: LobeToolManifest[];
/** Default tool IDs that will always be added */
defaultToolIds?: string[];
/** Custom enable checker for plugins */
@@ -33,6 +32,8 @@ export interface ServerAgentToolsEngineConfig {
* Parameters for createServerAgentToolsEngine
*/
export interface ServerCreateAgentToolsEngineParams {
/** Additional manifests to include (e.g., LobeHub Skills) */
additionalManifests?: LobeToolManifest[];
/** Agent configuration containing plugins array */
agentConfig: {
/** Optional agent chat config with searchMode */

View File

@@ -642,6 +642,16 @@ export const aiAgentRouter = router({
const updatedStatus = updatedThread?.status ?? thread.status;
const updatedTaskStatus = threadStatusToTaskStatus[updatedStatus] || 'processing';
// DEBUG: Log metadata for failed tasks
if (updatedTaskStatus === 'failed') {
console.log('[DEBUG] getSubAgentTaskStatus - failed task metadata:', {
threadId,
updatedStatus,
'updatedMetadata?.error': updatedMetadata?.error,
updatedMetadata,
});
}
// 6. Query thread messages for result content or current activity
const threadMessages = await ctx.messageModel.query({ threadId });
const sortedMessages = threadMessages.sort(

View File

@@ -216,6 +216,161 @@ describe('AgentService', () => {
});
});
describe('getAgentConfig', () => {
it('should return null if agent does not exist', async () => {
const mockAgentModel = {
getAgentConfig: vi.fn().mockResolvedValue(null),
};
(AgentModel as any).mockImplementation(() => mockAgentModel);
(parseAgentConfig as any).mockReturnValue({});
const newService = new AgentService(mockDb, mockUserId);
const result = await newService.getAgentConfig('non-existent');
expect(result).toBeNull();
});
it('should support lookup by agent id', async () => {
const mockAgent = {
id: 'agent-123',
model: 'gpt-4',
systemRole: 'Test role',
};
const mockAgentModel = {
getAgentConfig: vi.fn().mockResolvedValue(mockAgent),
};
(AgentModel as any).mockImplementation(() => mockAgentModel);
(parseAgentConfig as any).mockReturnValue({});
const newService = new AgentService(mockDb, mockUserId);
const result = await newService.getAgentConfig('agent-123');
expect(mockAgentModel.getAgentConfig).toHaveBeenCalledWith('agent-123');
expect(result?.id).toBe('agent-123');
expect(result?.model).toBe('gpt-4');
});
it('should support lookup by slug', async () => {
const mockAgent = {
id: 'agent-123',
model: 'claude-3',
slug: 'my-agent',
};
const mockAgentModel = {
getAgentConfig: vi.fn().mockResolvedValue(mockAgent),
};
(AgentModel as any).mockImplementation(() => mockAgentModel);
(parseAgentConfig as any).mockReturnValue({});
const newService = new AgentService(mockDb, mockUserId);
const result = await newService.getAgentConfig('my-agent');
expect(mockAgentModel.getAgentConfig).toHaveBeenCalledWith('my-agent');
expect(result?.id).toBe('agent-123');
});
it('should merge DEFAULT_AGENT_CONFIG and serverDefaultAgentConfig with agent config', async () => {
const mockAgent = {
id: 'agent-1',
systemRole: 'Custom system role',
};
const serverDefaultConfig = { model: 'gpt-4', params: { temperature: 0.7 } };
const mockAgentModel = {
getAgentConfig: vi.fn().mockResolvedValue(mockAgent),
};
(AgentModel as any).mockImplementation(() => mockAgentModel);
(parseAgentConfig as any).mockReturnValue(serverDefaultConfig);
const newService = new AgentService(mockDb, mockUserId);
const result = await newService.getAgentConfig('agent-1');
expect(result).toMatchObject({
chatConfig: DEFAULT_AGENT_CONFIG.chatConfig,
plugins: DEFAULT_AGENT_CONFIG.plugins,
tts: DEFAULT_AGENT_CONFIG.tts,
model: 'gpt-4',
params: { temperature: 0.7 },
id: 'agent-1',
systemRole: 'Custom system role',
});
});
it('should use default model/provider when agent has none', async () => {
const mockAgent = {
id: 'agent-1',
systemRole: 'Test',
// No model or provider set
};
const mockAgentModel = {
getAgentConfig: vi.fn().mockResolvedValue(mockAgent),
};
(AgentModel as any).mockImplementation(() => mockAgentModel);
(parseAgentConfig as any).mockReturnValue({});
const newService = new AgentService(mockDb, mockUserId);
const result = await newService.getAgentConfig('agent-1');
// Should have default model/provider from DEFAULT_AGENT_CONFIG
expect(result?.model).toBe(DEFAULT_AGENT_CONFIG.model);
expect(result?.provider).toBe(DEFAULT_AGENT_CONFIG.provider);
});
it('should prioritize agent model/provider over defaults', async () => {
const mockAgent = {
id: 'agent-1',
model: 'claude-3-opus',
provider: 'anthropic',
};
const serverDefaultConfig = { model: 'gpt-4', provider: 'openai' };
const mockAgentModel = {
getAgentConfig: vi.fn().mockResolvedValue(mockAgent),
};
(AgentModel as any).mockImplementation(() => mockAgentModel);
(parseAgentConfig as any).mockReturnValue(serverDefaultConfig);
const newService = new AgentService(mockDb, mockUserId);
const result = await newService.getAgentConfig('agent-1');
// Agent config should override server default
expect(result?.model).toBe('claude-3-opus');
expect(result?.provider).toBe('anthropic');
});
it('should merge user default agent config', async () => {
const mockAgent = {
id: 'agent-1',
};
const userDefaultConfig = { model: 'user-preferred-model', provider: 'user-provider' };
const mockAgentModel = {
getAgentConfig: vi.fn().mockResolvedValue(mockAgent),
};
(AgentModel as any).mockImplementation(() => mockAgentModel);
(parseAgentConfig as any).mockReturnValue({});
// Use mockResolvedValueOnce to avoid affecting subsequent tests
mockUserModel.getUserSettingsDefaultAgentConfig.mockResolvedValueOnce({ config: userDefaultConfig });
const newService = new AgentService(mockDb, mockUserId);
const result = await newService.getAgentConfig('agent-1');
// User default config should be applied
expect(result?.model).toBe('user-preferred-model');
expect(result?.provider).toBe('user-provider');
});
});
describe('getAgentConfigById', () => {
it('should return null if agent does not exist', async () => {
const mockAgentModel = {

View File

@@ -17,6 +17,12 @@ import { type UpdateAgentResult } from './type';
const log = debug('lobe-agent:service');
/**
* Agent config with required id field.
* Used when returning agent config from database (id is always present).
*/
export type AgentConfigWithId = LobeAgentConfig & { id: string };
interface AgentWelcomeData {
openQuestions: string[];
welcomeMessage: string;
@@ -78,6 +84,25 @@ export class AgentService {
return mergedConfig;
}
/**
* Get agent config by ID or slug with default config merged.
* Supports both agentId and slug lookup.
*
* The returned agent config is merged with:
* 1. DEFAULT_AGENT_CONFIG (hardcoded defaults)
* 2. Server's globalDefaultAgentConfig (from environment variable DEFAULT_AGENT_CONFIG)
* 3. User's defaultAgentConfig (from user settings)
* 4. The actual agent config from database
*/
async getAgentConfig(idOrSlug: string): Promise<AgentConfigWithId | null> {
const [agent, defaultAgentConfig] = await Promise.all([
this.agentModel.getAgentConfig(idOrSlug),
this.userModel.getUserSettingsDefaultAgentConfig(),
]);
return this.mergeDefaultConfig(agent, defaultAgentConfig) as AgentConfigWithId | null;
}
/**
* Get agent config by ID with default config merged.
*

View File

@@ -231,6 +231,7 @@ export class AgentRuntimeService {
initialMessages = [],
appContext,
toolManifestMap,
toolSourceMap,
stepCallbacks,
} = params;
@@ -258,6 +259,7 @@ export class AgentRuntimeService {
status: 'idle',
stepCount: 0,
toolManifestMap,
toolSourceMap,
tools,
} as Partial<AgentState>;

View File

@@ -87,6 +87,7 @@ export interface OperationCreationParams {
*/
stepCallbacks?: StepLifecycleCallbacks;
toolManifestMap: Record<string, LobeToolManifest>;
toolSourceMap?: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis' | 'lobehubSkill'>;
tools?: any[];
userId?: string;
}

View File

@@ -29,6 +29,22 @@ vi.mock('@/database/models/agent', () => ({
})),
}));
// Mock AgentService
vi.mock('@/server/services/agent', () => ({
AgentService: vi.fn().mockImplementation(() => ({
getAgentConfig: vi.fn().mockResolvedValue({
chatConfig: {},
files: [],
id: 'agent-1',
knowledgeBases: [],
model: 'gpt-4',
plugins: [],
provider: 'openai',
systemRole: 'You are a helpful assistant',
}),
})),
}));
// Mock PluginModel
vi.mock('@/database/models/plugin', () => ({
PluginModel: vi.fn().mockImplementation(() => ({
@@ -74,15 +90,19 @@ vi.mock('@/server/modules/Mecha', () => ({
}));
// Mock model-bank
vi.mock('model-bank', () => ({
LOBE_DEFAULT_MODEL_LIST: [
{
abilities: { functionCall: true, video: false, vision: true },
id: 'gpt-4',
providerId: 'openai',
},
],
}));
vi.mock('model-bank', async (importOriginal) => {
const actual = await importOriginal<typeof import('model-bank')>();
return {
...actual,
LOBE_DEFAULT_MODEL_LIST: [
{
abilities: { functionCall: true, video: false, vision: true },
id: 'gpt-4',
providerId: 'openai',
},
],
};
});
describe('AiAgentService.execAgent - threadId handling', () => {
let service: AiAgentService;

View File

@@ -1,4 +1,5 @@
import type { AgentRuntimeContext, AgentState } from '@lobechat/agent-runtime';
import type { LobeToolManifest } from '@lobechat/context-engine';
import { type LobeChatDatabase } from '@lobechat/database';
import type {
ExecAgentParams,
@@ -10,6 +11,7 @@ import type {
} from '@lobechat/types';
import { ThreadStatus, ThreadType } from '@lobechat/types';
import { nanoid } from '@lobechat/utils';
import { MarketSDK } from '@lobehub/market-sdk';
import debug from 'debug';
import { LOADING_FLAT } from '@/const/message';
@@ -18,16 +20,43 @@ import { MessageModel } from '@/database/models/message';
import { PluginModel } from '@/database/models/plugin';
import { ThreadModel } from '@/database/models/thread';
import { TopicModel } from '@/database/models/topic';
import { UserModel } from '@/database/models/user';
import { generateTrustedClientToken } from '@/libs/trusted-client';
import {
type ServerAgentToolsContext,
createServerAgentToolsEngine,
serverMessagesEngine,
} from '@/server/modules/Mecha';
import { AgentService } from '@/server/services/agent';
import { AgentRuntimeService } from '@/server/services/agentRuntime';
import type { StepLifecycleCallbacks } from '@/server/services/agentRuntime/types';
const log = debug('lobe-server:ai-agent-service');
/**
* Format error for storage in thread metadata
* Handles Error objects which don't serialize properly with JSON.stringify
*/
function formatErrorForMetadata(error: unknown): Record<string, any> | undefined {
if (!error) return undefined;
// Handle Error objects
if (error instanceof Error) {
return {
message: error.message,
name: error.name,
};
}
// Handle objects with message property (like ChatMessageError)
if (typeof error === 'object' && 'message' in error) {
return error as Record<string, any>;
}
// Fallback: wrap in object
return { message: String(error) };
}
/**
* Internal params for execAgent with step lifecycle callbacks
* This extends the public ExecAgentParams with server-side only options
@@ -53,6 +82,7 @@ export class AiAgentService {
private readonly userId: string;
private readonly db: LobeChatDatabase;
private readonly agentModel: AgentModel;
private readonly agentService: AgentService;
private readonly messageModel: MessageModel;
private readonly pluginModel: PluginModel;
private readonly threadModel: ThreadModel;
@@ -63,6 +93,7 @@ export class AiAgentService {
this.userId = userId;
this.db = db;
this.agentModel = new AgentModel(db, userId);
this.agentService = new AgentService(db, userId);
this.messageModel = new MessageModel(db, userId);
this.pluginModel = new PluginModel(db, userId);
this.threadModel = new ThreadModel(db, userId);
@@ -106,8 +137,8 @@ export class AiAgentService {
log('execAgent: identifier=%s, prompt=%s', identifier, prompt.slice(0, 50));
// 1. Get agent configuration from database (supports both id and slug)
const agentConfig = await this.agentModel.getAgentConfig(identifier);
// 1. Get agent configuration with default config merged (supports both id and slug)
const agentConfig = await this.agentService.getAgentConfig(identifier);
if (!agentConfig) {
throw new Error(`Agent not found: ${identifier}`);
}
@@ -158,7 +189,11 @@ export class AiAgentService {
return info?.abilities?.functionCall ?? true;
};
// 5. Create tools using Server AgentToolsEngine
// 5. Fetch LobeHub Skills manifests (temporary solution until LOBE-3517 is implemented)
const lobehubSkillManifests = await this.fetchLobehubSkillManifests();
log('execAgent: got %d lobehub skill manifests', lobehubSkillManifests.length);
// 6. Create tools using Server AgentToolsEngine
const hasEnabledKnowledgeBases =
agentConfig.knowledgeBases?.some((kb: { enabled?: boolean | null }) => kb.enabled === true) ??
false;
@@ -169,6 +204,7 @@ export class AiAgentService {
};
const toolsEngine = createServerAgentToolsEngine(toolsContext, {
additionalManifests: lobehubSkillManifests,
agentConfig: {
chatConfig: agentConfig.chatConfig ?? undefined,
plugins: agentConfig.plugins ?? undefined,
@@ -180,6 +216,8 @@ export class AiAgentService {
// Generate tools and manifest map
const pluginIds = agentConfig.plugins || [];
log('execAgent: agent configured plugins: %O', pluginIds);
const toolsResult = toolsEngine.generateToolsDetailed({
model,
provider,
@@ -188,6 +226,12 @@ export class AiAgentService {
const tools = toolsResult.tools;
// Log detailed tools generation result
if (toolsResult.filteredTools && toolsResult.filteredTools.length > 0) {
log('execAgent: filtered tools: %O', toolsResult.filteredTools);
}
log('execAgent: enabled tool ids: %O', toolsResult.enabledToolIds);
// Get manifest map and convert from Map to Record
const manifestMap = toolsEngine.getEnabledPluginManifests(pluginIds);
const toolManifestMap: Record<string, any> = {};
@@ -195,7 +239,20 @@ export class AiAgentService {
toolManifestMap[id] = manifest;
});
log('execAgent: generated %d tools', tools?.length ?? 0);
// Build toolSourceMap for routing tool execution
const toolSourceMap: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis' | 'lobehubSkill'> =
{};
// Mark lobehub skills
for (const manifest of lobehubSkillManifests) {
toolSourceMap[manifest.identifier] = 'lobehubSkill';
}
log(
'execAgent: generated %d tools from %d configured plugins, %d lobehub skills',
tools?.length ?? 0,
pluginIds.length,
lobehubSkillManifests.length,
);
// 6. Get existing messages if provided
let historyMessages: any[] = [];
@@ -301,7 +358,18 @@ export class AiAgentService {
},
};
// 12. Create operation using AgentRuntimeService
// 12. Log final operation parameters summary
log(
'execAgent: creating operation %s with params: model=%s, provider=%s, tools=%d, messages=%d, manifests=%d',
operationId,
model,
provider,
tools?.length ?? 0,
processedMessages.length,
Object.keys(toolManifestMap).length,
);
// 13. Create operation using AgentRuntimeService
// Wrap in try-catch to handle operation startup failures (e.g., QStash unavailable)
// If createOperation fails, we still have valid messages that need error info
try {
@@ -320,6 +388,7 @@ export class AiAgentService {
operationId,
stepCallbacks,
toolManifestMap,
toolSourceMap,
tools,
userId: this.userId,
});
@@ -616,6 +685,11 @@ export class AiAgentService {
}
}
// Log error when task fails
if (reason === 'error' && finalState.error) {
console.error('execSubAgentTask: task failed for thread %s:', threadId, finalState.error);
}
try {
// Extract summary from last assistant message and update task message content
const lastAssistantMessage = finalState.messages
@@ -630,12 +704,15 @@ export class AiAgentService {
log('execSubAgentTask: updated task message %s with summary', sourceMessageId);
}
// Format error for proper serialization (Error objects don't serialize with JSON.stringify)
const formattedError = formatErrorForMetadata(finalState.error);
// Update Thread metadata
await this.threadModel.update(threadId, {
metadata: {
completedAt,
duration,
error: finalState.error,
error: formattedError,
operationId: finalState.operationId,
startedAt,
totalCost: finalState.cost?.total,
@@ -659,6 +736,98 @@ export class AiAgentService {
};
}
/**
* Fetch LobeHub Skills manifests from Market API
* This is a temporary solution until LOBE-3517 is implemented (store skills in DB)
*/
private async fetchLobehubSkillManifests(): Promise<LobeToolManifest[]> {
try {
// 1. Get user info for trusted client token
const user = await UserModel.findById(this.db, this.userId);
if (!user?.email) {
log('fetchLobehubSkillManifests: user email not found, skipping');
return [];
}
// 2. Generate trusted client token
const trustedClientToken = generateTrustedClientToken({
email: user.email,
name: user.fullName || user.firstName || undefined,
userId: this.userId,
});
if (!trustedClientToken) {
log('fetchLobehubSkillManifests: trusted client not configured, skipping');
return [];
}
// 3. Create MarketSDK instance
const marketSDK = new MarketSDK({
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
trustedClientToken,
});
// 4. Get user's connected skills
const { connections } = await marketSDK.connect.listConnections();
if (!connections || connections.length === 0) {
log('fetchLobehubSkillManifests: no connected skills found');
return [];
}
log('fetchLobehubSkillManifests: found %d connected skills', connections.length);
// 5. Fetch tools for each connection and build manifests
const manifests: LobeToolManifest[] = [];
for (const connection of connections) {
try {
// Connection returns providerId (e.g., 'twitter', 'linear'), not numeric id
const providerId = (connection as any).providerId;
if (!providerId) {
log('fetchLobehubSkillManifests: connection missing providerId: %O', connection);
continue;
}
const providerName =
(connection as any).providerName || (connection as any).name || providerId;
const icon = (connection as any).icon;
const { tools } = await marketSDK.skills.listTools(providerId);
if (!tools || tools.length === 0) continue;
const manifest: LobeToolManifest = {
api: tools.map((tool: any) => ({
description: tool.description || '',
name: tool.name,
parameters: tool.inputSchema || { properties: {}, type: 'object' },
})),
identifier: providerId,
meta: {
avatar: icon || '🔗',
description: `LobeHub Skill: ${providerName}`,
tags: ['lobehub-skill', providerId],
title: providerName,
},
type: 'builtin',
};
manifests.push(manifest);
log(
'fetchLobehubSkillManifests: built manifest for %s with %d tools',
providerId,
tools.length,
);
} catch (error) {
log('fetchLobehubSkillManifests: failed to fetch tools for connection: %O', error);
}
}
return manifests;
} catch (error) {
log('fetchLobehubSkillManifests: error fetching skills: %O', error);
return [];
}
}
/**
* Calculate total tokens from AgentState usage object
* AgentState.usage is of type Usage from @lobechat/agent-runtime

View File

@@ -0,0 +1,109 @@
import { type LobeChatDatabase } from '@lobechat/database';
import { MarketSDK } from '@lobehub/market-sdk';
import debug from 'debug';
import { UserModel } from '@/database/models/user';
import { generateTrustedClientToken } from '@/libs/trusted-client';
const log = debug('lobe-server:lobehub-skill-service');
export interface LobehubSkillExecuteParams {
args: Record<string, any>;
provider: string;
toolName: string;
}
export interface LobehubSkillExecuteResult {
content: string;
error?: { code: string; message?: string };
success: boolean;
}
export class LobehubSkillService {
private db: LobeChatDatabase;
private userId: string;
private marketSDK?: MarketSDK;
constructor(db: LobeChatDatabase, userId: string) {
this.db = db;
this.userId = userId;
}
/**
* Initialize MarketSDK with trusted client token
*/
private async getMarketSDK(): Promise<MarketSDK | null> {
if (this.marketSDK) return this.marketSDK;
try {
const user = await UserModel.findById(this.db, this.userId);
if (!user?.email) {
log('getMarketSDK: user email not found');
return null;
}
const trustedClientToken = generateTrustedClientToken({
email: user.email,
name: user.fullName || user.firstName || undefined,
userId: this.userId,
});
if (!trustedClientToken) {
log('getMarketSDK: trusted client not configured');
return null;
}
this.marketSDK = new MarketSDK({
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
trustedClientToken,
});
return this.marketSDK;
} catch (error) {
log('getMarketSDK: error creating SDK: %O', error);
return null;
}
}
/**
* Execute a LobeHub Skill tool
*/
async execute(params: LobehubSkillExecuteParams): Promise<LobehubSkillExecuteResult> {
const { provider, toolName, args } = params;
log('execute: %s/%s with args: %O', provider, toolName, args);
const sdk = await this.getMarketSDK();
if (!sdk) {
return {
content:
'MarketSDK not available. Please ensure you are authenticated with LobeHub Market.',
error: { code: 'MARKET_SDK_NOT_AVAILABLE' },
success: false,
};
}
try {
const response = await sdk.skills.callTool(provider, {
args,
tool: toolName,
});
log('execute: response: %O', response);
return {
content: typeof response.data === 'string' ? response.data : JSON.stringify(response.data),
success: response.success,
};
} catch (error) {
const err = error as Error;
console.error('LobehubSkillService.execute error %s/%s: %O', provider, toolName, err);
return {
content: err.message,
error: { code: 'LOBEHUB_SKILL_ERROR', message: err.message },
success: false,
};
}
}
}

View File

@@ -4,6 +4,7 @@ import { type ChatToolPayload } from '@lobechat/types';
import { safeParseJSON } from '@lobechat/utils';
import debug from 'debug';
import { LobehubSkillService } from '@/server/services/lobehubSkill';
import { SearchService } from '@/server/services/search';
import { type IToolExecutor, type ToolExecutionContext, type ToolExecutionResult } from './types';
@@ -19,11 +20,36 @@ export class BuiltinToolsExecutor implements IToolExecutor {
payload: ChatToolPayload,
context: ToolExecutionContext,
): Promise<ToolExecutionResult> {
const { identifier, apiName, arguments: argsStr } = payload;
const { identifier, apiName, arguments: argsStr, source } = payload;
const args = safeParseJSON(argsStr) || {};
log('Executing builtin tool: %s:%s with args: %O', identifier, apiName, args, context);
log(
'Executing builtin tool: %s:%s (source: %s) with args: %O',
identifier,
apiName,
source,
args,
);
// Route LobeHub Skills to dedicated service
if (source === 'lobehubSkill') {
if (!context.serverDB || !context.userId) {
return {
content: 'Server context not available for LobeHub Skills execution.',
error: { code: 'CONTEXT_NOT_AVAILABLE' },
success: false,
};
}
const skillService = new LobehubSkillService(context.serverDB, context.userId);
return skillService.execute({
args,
provider: identifier,
toolName: apiName,
});
}
// Default: original builtin runtime logic
const ServerRuntime = BuiltinToolServerRuntimes[identifier];
if (!ServerRuntime) {

View File

@@ -1,7 +1,10 @@
import { type LobeToolManifest } from '@lobechat/context-engine';
import { type LobeChatDatabase } from '@lobechat/database';
import { type ChatToolPayload } from '@lobechat/types';
export interface ToolExecutionContext {
/** Server database for LobeHub Skills execution */
serverDB?: LobeChatDatabase;
toolManifestMap: Record<string, LobeToolManifest>;
userId?: string;
}

View File

@@ -1087,12 +1087,14 @@ export const createAgentExecutors = (context: {
}
if (status.status === 'failed') {
log('[%s] Task failed: %s', taskLogId, status.error);
// Extract error message (error is always a string in TaskStatusResult)
const errorMessage = status.error || 'Unknown error';
log('[%s] Task failed: %s', taskLogId, errorMessage);
await context
.get()
.optimisticUpdateMessageContent(
taskMessageId,
`Task failed: ${status.error}`,
`Task failed: ${errorMessage}`,
undefined,
{ operationId: state.operationId },
);