Files
lobe-chat/src/services/chat/mecha/contextEngineering.ts
T
2026-06-04 16:23:51 +08:00

780 lines
28 KiB
TypeScript

import { LobeActivatorIdentifier } from '@lobechat/builtin-tool-activator';
import { AgentBuilderIdentifier } from '@lobechat/builtin-tool-agent-builder';
import { AgentManagementIdentifier } from '@lobechat/builtin-tool-agent-management';
import {
CredsIdentifier,
type CredSummary,
generateCredsList,
generateKlavisServicesList,
type KlavisServiceSummary,
} from '@lobechat/builtin-tool-creds';
import { GroupAgentBuilderIdentifier } from '@lobechat/builtin-tool-group-agent-builder';
import { LobeAgentIdentifier } from '@lobechat/builtin-tool-lobe-agent';
import { PageAgentIdentifier } from '@lobechat/builtin-tool-page-agent';
import { WebOnboardingIdentifier } from '@lobechat/builtin-tool-web-onboarding';
import { KLAVIS_SERVER_TYPES, LOBEHUB_SKILL_PROVIDERS } from '@lobechat/const';
import type {
AgentBuilderContext,
AgentContextDocument,
AgentGroupConfig,
AgentManagementContext,
GroupAgentBuilderContext,
GroupOfficialToolItem,
LobeToolManifest,
MemoryContext,
OnboardingContext,
PlanTodoConfig,
ToolDiscoveryConfig,
UserMemoryData,
} from '@lobechat/context-engine';
import { MessagesEngine, resolveTopicReferences } from '@lobechat/context-engine';
import { historySummaryPrompt } from '@lobechat/prompts';
import type {
OpenAIChatMessage,
RuntimeInitialContext,
RuntimeStepContext,
UIChatMessage,
} from '@lobechat/types';
import debug from 'debug';
import { isCanUseFC } from '@/helpers/isCanUseFC';
import { VARIABLE_GENERATORS } from '@/helpers/parserPlaceholder';
import { lambdaClient } from '@/libs/trpc/client';
import {
agentService,
AVAILABLE_AGENTS_CONTEXT_LIMIT,
AVAILABLE_AGENTS_CONTEXT_QUERY_LIMIT,
} from '@/services/agent';
import { notebookService } from '@/services/notebook';
import { getAgentStoreState } from '@/store/agent';
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
import { getChatGroupStoreState } from '@/store/agentGroup';
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
import { getAiInfraStoreState } from '@/store/aiInfra';
import { getChatStoreState } from '@/store/chat';
import { topicSelectors } from '@/store/chat/selectors';
import { getToolStoreState } from '@/store/tool';
import {
builtinToolSelectors,
klavisStoreSelectors,
lobehubSkillStoreSelectors,
toolSelectors,
} from '@/store/tool/selectors';
import { KlavisServerStatus } from '@/store/tool/slices/klavisStore';
import { isCanUseVideo, isCanUseVision } from '../helper';
import { combineUserMemoryData, resolveTopicMemories, resolveUserPersona } from './memoryManager';
import { resolveClientSkills } from './skillEngineering';
const log = debug('context-engine:contextEngineering');
interface ContextEngineeringContext {
/** Agent Builder context for injecting current agent info */
agentBuilderContext?: AgentBuilderContext;
agentDocuments?: AgentContextDocument[];
/** The agent ID that will respond (for group context injection) */
agentId?: string;
enableHistoryCount?: boolean;
enableUserMemories?: boolean;
/** Group ID for multi-agent scenarios */
groupId?: string;
historyCount?: number;
historySummary?: string;
/**
* Initial context from Agent Runtime
* Contains markdown and metadata captured at operation start
*/
initialContext?: RuntimeInitialContext;
inputTemplate?: string;
/** Tool manifests with systemRole and API definitions */
manifests?: LobeToolManifest[];
/** Memory-related context for prompt/runtime behavior */
memoryContext?: MemoryContext;
messages: UIChatMessage[];
model: string;
/** Agent's enabled plugin/tool/skill identifiers (from agentConfig.plugins) */
plugins?: string[];
provider: string;
sessionId?: string;
/**
* Step context from Agent Runtime
* Contains latest XML structure updated each step
*/
stepContext?: RuntimeStepContext;
systemRole?: string;
tools?: string[];
/** Topic ID for plan/todo context injection */
topicId?: string;
}
// REVIEW: Maybe we can constrain identity, preference, exp to reorder or trim the context instead of passing everything in
export const contextEngineering = async ({
messages = [],
manifests,
tools,
model,
provider,
systemRole,
inputTemplate,
enableUserMemories,
enableHistoryCount,
historyCount,
historySummary,
agentBuilderContext,
agentDocuments,
agentId,
groupId,
initialContext,
plugins,
stepContext,
topicId,
memoryContext,
}: ContextEngineeringContext): Promise<OpenAIChatMessage[]> => {
log('tools: %o', tools);
// Check if Agent Builder tool is enabled
const isAgentBuilderEnabled = tools?.includes(AgentBuilderIdentifier) ?? false;
// Check if Group Agent Builder tool is enabled
const isGroupAgentBuilderEnabled = tools?.includes(GroupAgentBuilderIdentifier) ?? false;
// Check if Agent Management tool is enabled
const isAgentManagementEnabled = tools?.includes(AgentManagementIdentifier) ?? false;
log('isAgentBuilderEnabled: %s', isAgentBuilderEnabled);
log('isGroupAgentBuilderEnabled: %s', isGroupAgentBuilderEnabled);
log('isAgentManagementEnabled: %s', isAgentManagementEnabled);
// Build agent group configuration if groupId is provided
let agentGroup: AgentGroupConfig | undefined;
if (groupId) {
const groupStoreState = getChatGroupStoreState();
const groupDetail = agentGroupSelectors.getGroupById(groupId)(groupStoreState);
if (groupDetail?.agents && groupDetail.agents.length > 0) {
const agentMap: AgentGroupConfig['agentMap'] = {};
const members: AgentGroupConfig['members'] = [];
// Find the responding agent to get its name and role
let currentAgentName: string | undefined;
let currentAgentRole: 'supervisor' | 'participant' | undefined;
for (const agent of groupDetail.agents) {
const role = agent.isSupervisor ? 'supervisor' : 'participant';
const name = agent.title || 'Untitled Agent';
agentMap[agent.id] = { name, role };
members.push({ id: agent.id, name, role });
// Capture responding agent info
if (agentId && agent.id === agentId) {
currentAgentName = name;
currentAgentRole = role;
}
}
agentGroup = {
agentMap,
currentAgentId: agentId,
currentAgentName,
currentAgentRole,
groupTitle: groupDetail.title || undefined,
members,
// Use group.content as the group description (shared prompt/content)
systemPrompt: groupDetail.content || undefined,
};
log('agentGroup built: %o', agentGroup);
}
}
// Get agent store state (used for both group agent builder context and file/knowledge base)
const agentStoreState = getAgentStoreState();
// Build group agent builder context if Group Agent Builder is enabled
// Note: Uses activeGroupId from chatStore to get the group being edited
let groupAgentBuilderContext: GroupAgentBuilderContext | undefined;
if (isGroupAgentBuilderEnabled) {
const activeGroupId = getChatStoreState().activeGroupId;
if (activeGroupId) {
const groupStoreState = getChatGroupStoreState();
const activeGroupDetail = agentGroupSelectors.getGroupById(activeGroupId)(groupStoreState);
if (activeGroupDetail) {
// Get supervisor agent config if supervisorAgentId exists
let supervisorConfig: GroupAgentBuilderContext['supervisorConfig'];
let enabledPlugins: string[] = [];
if (activeGroupDetail.supervisorAgentId) {
const supervisorAgentConfig = agentSelectors.getAgentConfigById(
activeGroupDetail.supervisorAgentId,
)(agentStoreState);
supervisorConfig = {
model: supervisorAgentConfig.model,
plugins: supervisorAgentConfig.plugins,
provider: supervisorAgentConfig.provider,
};
enabledPlugins = supervisorAgentConfig.plugins || [];
}
// Build official tools list (builtin tools + Klavis tools)
const toolState = getToolStoreState();
const officialTools: GroupOfficialToolItem[] = [];
// Get builtin tools (excluding Klavis tools)
const builtinTools = builtinToolSelectors.metaList(toolState);
const klavisIdentifiers = new Set(KLAVIS_SERVER_TYPES.map((t) => t.identifier));
for (const tool of builtinTools) {
// Skip Klavis tools in builtin list (they'll be shown separately)
if (klavisIdentifiers.has(tool.identifier)) continue;
officialTools.push({
description: tool.meta?.description,
enabled: enabledPlugins.includes(tool.identifier),
identifier: tool.identifier,
installed: true,
name: tool.meta?.title || tool.identifier,
type: 'builtin',
});
}
// Get Klavis tools (if enabled)
const isKlavisEnabled =
typeof window !== 'undefined' &&
window.global_serverConfigStore?.getState()?.serverConfig?.enableKlavis;
if (isKlavisEnabled) {
const allKlavisServers = klavisStoreSelectors.getServers(toolState);
for (const klavisType of KLAVIS_SERVER_TYPES) {
const server = allKlavisServers.find((s) => s.identifier === klavisType.identifier);
officialTools.push({
description: `LobeHub Mcp Server: ${klavisType.label}`,
enabled: enabledPlugins.includes(klavisType.identifier),
identifier: klavisType.identifier,
installed: !!server,
name: klavisType.label,
type: 'klavis',
});
}
}
// Get LobehubSkill providers (if enabled)
const isLobehubSkillEnabled =
typeof window !== 'undefined' &&
window.global_serverConfigStore?.getState()?.serverConfig?.enableLobehubSkill;
if (isLobehubSkillEnabled) {
const allLobehubSkillServers = lobehubSkillStoreSelectors.getServers(toolState);
for (const provider of LOBEHUB_SKILL_PROVIDERS) {
const server = allLobehubSkillServers.find((s) => s.identifier === provider.id);
officialTools.push({
description: `LobeHub Skill Provider: ${provider.label}`,
enabled: enabledPlugins.includes(provider.id),
identifier: provider.id,
installed: !!server,
name: provider.label,
type: 'lobehub-skill',
});
}
}
groupAgentBuilderContext = {
config: {
openingMessage: activeGroupDetail.config?.openingMessage || undefined,
openingQuestions: activeGroupDetail.config?.openingQuestions,
systemPrompt: activeGroupDetail.config?.systemPrompt || undefined,
},
groupId: activeGroupId,
groupTitle: activeGroupDetail.title || undefined,
members: activeGroupDetail.agents?.map((agent) => ({
description: agent.description || undefined,
id: agent.id,
isSupervisor: agent.isSupervisor,
title: agent.title || 'Untitled Agent',
})),
officialTools,
supervisorConfig,
};
log('groupAgentBuilderContext built from activeGroupId: %o', groupAgentBuilderContext);
}
}
}
// Get enabled agent files with content and knowledge bases from agent store
const agentFiles = agentSelectors.currentAgentFiles(agentStoreState);
const agentKnowledgeBases = agentSelectors.currentAgentKnowledgeBases(agentStoreState);
const fileContents = agentFiles
.filter((file) => file.enabled && file.content)
.map((file) => ({ content: file.content!, fileId: file.id, filename: file.name }));
const knowledgeBases = agentKnowledgeBases
.filter((kb) => kb.enabled)
.map((kb) => ({ description: kb.description, id: kb.id, name: kb.name }));
// Resolve user memories: topic memories and user persona are independent layers
// Both functions now read from cache only (no network requests) to avoid blocking sendMessage
let userMemoryData: UserMemoryData | undefined;
if (enableUserMemories) {
const topicMemories = resolveTopicMemories();
const persona = resolveUserPersona();
userMemoryData = combineUserMemoryData(topicMemories, persona);
}
// Resolve plan + todos context (now part of the lobe-agent tool).
// Lobe-agent must be enabled and topicId must be provided.
const isPlanTodoEnabled = tools?.includes(LobeAgentIdentifier) ?? false;
let planTodoConfig: PlanTodoConfig | undefined;
if (isPlanTodoEnabled && topicId) {
try {
// Fetch plan document for the current topic
const planResult = await notebookService.listDocuments({
topicId,
type: 'agent/plan',
});
if (planResult.data.length > 0) {
const planDoc = planResult.data[0]; // Most recent plan
// Build plan object for injection
const plan = {
completed: false, // TODO: Add completed field to document if needed
context: planDoc.content ?? undefined,
createdAt: planDoc.createdAt.toISOString(),
description: planDoc.description || '',
goal: planDoc.title || '',
id: planDoc.id,
updatedAt: planDoc.updatedAt.toISOString(),
};
// Get todos from plan's metadata
const todos = planDoc.metadata?.todos;
planTodoConfig = {
enabled: true,
plan,
todos,
};
log('Plan/Todo context resolved: plan=%s, todos=%o', plan.goal, todos?.items?.length ?? 0);
}
} catch (error) {
// Silently fail - plan/todo context is optional
log('Failed to resolve plan/todo context:', error);
}
}
// Resolve user credentials context for creds tool
// Creds tool must be enabled to fetch credentials
const isCredsEnabled = tools?.includes(CredsIdentifier) ?? false;
let credsList: CredSummary[] | undefined;
if (isCredsEnabled) {
try {
const credsResult = await lambdaClient.market.creds.list.query();
const userCreds = (credsResult as any)?.data ?? [];
credsList = userCreds.map(
(cred: any): CredSummary => ({
description: cred.description,
key: cred.key,
name: cred.name,
type: cred.type,
}),
);
log('Creds context resolved: count=%d', credsList?.length ?? 0);
} catch (error) {
// Silently fail - creds context is optional
log('Failed to resolve creds context:', error);
}
}
// Build Klavis services list for creds context
// Shows which Klavis services are connected (authorized) and which are available to connect
let klavisServicesList = '';
const isKlavisEnabled =
typeof window !== 'undefined' &&
window.global_serverConfigStore?.getState()?.serverConfig?.enableKlavis;
if (isCredsEnabled && isKlavisEnabled) {
try {
const toolState = getToolStoreState();
const allKlavisServers = klavisStoreSelectors.getServers(toolState);
const connected: KlavisServiceSummary[] = allKlavisServers
.filter((s) => s.status === KlavisServerStatus.CONNECTED)
.map((s) => ({ identifier: s.identifier, name: s.serverName }));
const connectedIds = new Set(connected.map((s) => s.identifier));
const available: KlavisServiceSummary[] = KLAVIS_SERVER_TYPES.filter(
(t) => !connectedIds.has(t.identifier),
).map((t) => ({ identifier: t.identifier, name: t.label }));
klavisServicesList = generateKlavisServicesList(connected, available);
log(
'Klavis services context resolved: connected=%d, available=%d',
connected.length,
available.length,
);
} catch (error) {
log('Failed to resolve Klavis services context:', error);
}
}
const userMemoryConfig =
enableUserMemories && userMemoryData
? {
enabled: enableUserMemories,
memories: userMemoryData,
}
: undefined;
// Build tool discovery config if lobe-activator is enabled
const enabledToolSet = new Set(tools || []);
const isLobeToolsEnabled = enabledToolSet.has(LobeActivatorIdentifier);
let toolDiscoveryConfig: ToolDiscoveryConfig | undefined;
if (isLobeToolsEnabled) {
const toolState = getToolStoreState();
const availableTools = toolSelectors
.availableToolsForDiscovery(toolState)
.filter((tool) => !enabledToolSet.has(tool.identifier));
if (availableTools.length > 0) {
toolDiscoveryConfig = { availableTools };
log('Tool discovery config built, available tools count: %d', availableTools.length);
}
}
// Build Agent Management context.
// - availableAgents is injected whenever the user is in auto skill mode (so the
// supervisor can decide to activate agent-management on its own) OR when the tool
// is explicitly enabled.
// - availableProviders / availablePlugins are only built when the tool is explicitly
// enabled, since they're solely needed for createAgent / updateAgent.
let agentManagementContext: AgentManagementContext | undefined;
const isInAutoSkillMode =
agentChatConfigSelectors.skillActivateMode(agentStoreState) !== 'manual';
const shouldInjectAvailableAgents = isInAutoSkillMode || isAgentManagementEnabled;
if (shouldInjectAvailableAgents) {
try {
const recentAgents =
agentStoreState.availableAgents ??
(await agentService.queryAgents({ limit: AVAILABLE_AGENTS_CONTEXT_QUERY_LIMIT }));
// Exclude current agent from `availableAgents`. The model is the current
// agent — its identity/persona is already established by `systemRole`, so
// we don't re-inject it here, and removing self from the list ensures the
// model never sees its own id in the agent-management context (so it
// cannot accidentally call itself via `callAgent`).
const otherAgents = agentId ? recentAgents.filter((a) => a.id !== agentId) : recentAgents;
const hasMoreAgents = otherAgents.length > AVAILABLE_AGENTS_CONTEXT_LIMIT;
const availableAgents = otherAgents.slice(0, AVAILABLE_AGENTS_CONTEXT_LIMIT).map((a) => ({
description: a.description ?? undefined,
id: a.id,
title: a.title ?? 'Untitled',
}));
agentManagementContext = {
availableAgents,
availableAgentsHasMore: hasMoreAgents,
...(agentId && {
currentAgent: {
id: agentId,
title: agentSelectors.getAgentMetaById(agentId)(agentStoreState)?.title ?? undefined,
},
}),
};
log('availableAgents fetched: %d agents (hasMore=%s)', availableAgents.length, hasMoreAgents);
} catch (error) {
// Silently fail - availableAgents context is optional
log('Failed to fetch availableAgents: %O', error);
}
}
if (isAgentManagementEnabled) {
// Get enabled providers and models from aiInfra store
const aiProviderState = getAiInfraStoreState();
const enabledChatModelList = aiProviderState.enabledChatModelList || [];
// Build availableProviders from enabled chat models (only user-enabled providers)
// Limit to first 5 providers to avoid context bloat
const availableProviders = enabledChatModelList.slice(0, 5).map((provider) => ({
id: provider.id,
models: provider.children.map((model) => ({
abilities: model.abilities,
description: model.description,
id: model.id,
name: model.displayName || model.id,
})),
name: provider.name,
}));
// Get tool state for plugins
const toolState = getToolStoreState();
// Build availablePlugins from all plugin sources
const availablePlugins = [];
// Builtin tools (use allMetaList to include hidden tools like web-browsing, cloud-sandbox, etc.)
// Exclude only truly internal tools (agent-management itself, agent-builder, page-agent)
const allBuiltinTools = builtinToolSelectors.allMetaList(toolState);
const klavisIdentifiers = new Set(KLAVIS_SERVER_TYPES.map((t) => t.identifier));
const INTERNAL_TOOLS = new Set([
'lobe-agent-management', // Don't show agent-management in its own context
'lobe-agent-builder', // Used for editing current agent, not for creating new agents
'lobe-group-agent-builder', // Used for editing current group, not for creating new agents
'lobe-page-agent', // Page-editor specific tool
]);
for (const tool of allBuiltinTools) {
// Skip Klavis tools in builtin list (they'll be shown separately)
if (klavisIdentifiers.has(tool.identifier)) continue;
// Skip internal tools
if (INTERNAL_TOOLS.has(tool.identifier)) continue;
availablePlugins.push({
description: tool.meta?.description,
identifier: tool.identifier,
name: tool.meta?.title || tool.identifier,
type: 'builtin' as const,
});
}
// Klavis tools (if enabled)
const isKlavisEnabled =
typeof window !== 'undefined' &&
window.global_serverConfigStore?.getState()?.serverConfig?.enableKlavis;
if (isKlavisEnabled) {
for (const klavisType of KLAVIS_SERVER_TYPES) {
availablePlugins.push({
description: klavisType.description,
identifier: klavisType.identifier,
name: klavisType.label,
type: 'klavis' as const,
});
}
}
// LobehubSkill providers (if enabled)
const isLobehubSkillEnabled =
typeof window !== 'undefined' &&
window.global_serverConfigStore?.getState()?.serverConfig?.enableLobehubSkill;
if (isLobehubSkillEnabled) {
for (const provider of LOBEHUB_SKILL_PROVIDERS) {
availablePlugins.push({
description: provider.description,
identifier: provider.id,
name: provider.label,
type: 'lobehub-skill' as const,
});
}
}
agentManagementContext = {
...agentManagementContext,
availablePlugins,
availableProviders,
};
log(
'agentManagementContext built: %d providers, %d plugins, %d agents',
agentManagementContext.availableProviders?.length ?? 0,
agentManagementContext.availablePlugins?.length ?? 0,
agentManagementContext.availableAgents?.length ?? 0,
);
}
// Inject mentionedAgents independently of isAgentManagementEnabled.
// When user @mentions an agent, delegation context must always be injected
// even if the agent doesn't have agent-management tool in its config.
const hasMentionedAgents =
initialContext?.mentionedAgents && initialContext.mentionedAgents.length > 0;
if (hasMentionedAgents) {
agentManagementContext = {
...agentManagementContext,
mentionedAgents: initialContext!.mentionedAgents,
};
log('mentionedAgents injected: %d agents', initialContext!.mentionedAgents!.length);
}
// Resolve topic references from messages containing <refer_topic> tags
const topicReferences =
(await resolveTopicReferences(
messages,
async (topicId: string) => {
const topic = topicSelectors.getTopicById(topicId)(getChatStoreState());
return topic ?? null;
},
async (topicId: string) => {
const { messageService } = await import('@/services/message');
const msgs = await messageService.getMessages({ agentId, groupId, topicId });
return msgs.map((m) => ({
content: typeof m.content === 'string' ? m.content : '',
role: m.role,
}));
},
)) ?? [];
// Build onboarding context if this is the web-onboarding agent.
// Single combined trpc call — server runs state/soul/persona DB queries in parallel.
let onboardingContext: OnboardingContext | undefined;
const isOnboardingAgent = tools?.includes(WebOnboardingIdentifier);
if (isOnboardingAgent) {
try {
const { userService } = await import('@/services/user');
onboardingContext = await userService.getOnboardingAgentContext();
log('Built onboarding context');
} catch (error) {
log('Failed to build onboarding context: %O', error);
}
}
// Create MessagesEngine with injected dependencies
const engine = new MessagesEngine({
// Agent configuration
enableHistoryCount,
formatHistorySummary: historySummaryPrompt,
historyCount,
historySummary,
inputTemplate,
systemRole,
// Capability injection
capabilities: {
isCanUseFC,
isCanUseVideo,
isCanUseVision,
},
// File context configuration
fileContext: { enabled: true, includeFileUrl: false },
// Knowledge injection
knowledge: {
fileContents,
knowledgeBases,
},
agentDocuments,
// Messages
messages,
// Model info
model,
provider,
// runtime context
initialContext,
stepContext,
// Selected skills/tools from user for this request
selectedSkills: initialContext?.selectedSkills,
selectedTools: initialContext?.selectedTools,
// Pass enableAgentMode through; MessagesEngine force-disables skills /
// agent-document injectors when this is `false` (chat mode).
enableAgentMode: agentChatConfigSelectors.currentChatConfig(agentStoreState).enableAgentMode,
// Skills configuration
// In auto mode: expose all installed skills so the AI can discover and activate them
// In manual mode: only expose user-selected skills (filtered by pluginIds)
skillsConfig: {
enabledSkills: plugins
? (() => {
const skillSet = resolveClientSkills(plugins);
if (!isInAutoSkillMode) {
const selectedIds = new Set(plugins);
return skillSet.skills.filter((s) => selectedIds.has(s.identifier));
}
return skillSet.skills;
})()
: undefined,
},
// Tool Discovery configuration
toolDiscoveryConfig,
// Tools configuration
toolsConfig: {
disabledToolIdentifiers: tools?.includes(PageAgentIdentifier)
? undefined
: [PageAgentIdentifier],
manifests,
tools,
},
// User memory configuration
userMemory: userMemoryConfig,
// Variable generators
variableGenerators: {
...VARIABLE_GENERATORS,
// NOTICE: required by builtin-tool-creds/src/systemRole.ts
CREDS_LIST: () => (credsList ? generateCredsList(credsList) : ''),
// NOTICE: required by builtin-tool-creds/src/systemRole.ts (Klavis integrations)
KLAVIS_SERVICES_LIST: () => klavisServicesList,
// NOTICE: required by builtin-tool-creds/src/systemRole.ts (session_context)
session_date: () =>
new Intl.DateTimeFormat('en-US', {
day: 'numeric',
month: 'long',
weekday: 'long',
year: 'numeric',
}).format(new Date()),
sandbox_enabled: () => String(tools?.includes('lobe-cloud-sandbox') ?? false),
// NOTICE(@nekomeowww): required by builtin-tool-memory/src/systemRole.ts
memory_effort: () => (userMemoryConfig ? (memoryContext?.effort ?? '') : ''),
// Current agent + topic identity — referenced by the LobeHub builtin
// skill (packages/builtin-skills/src/lobehub/content.ts) so the model
// can run `lh agent run -a {{agent_id}}` etc without first having to
// search for itself. Read lazily from stores so we only pay the cost
// when the placeholder actually appears in a rendered message.
agent_id: () => agentId ?? '',
agent_title: () =>
agentId ? (agentSelectors.getAgentMetaById(agentId)(agentStoreState)?.title ?? '') : '',
agent_description: () =>
agentId
? (agentSelectors.getAgentMetaById(agentId)(agentStoreState)?.description ?? '')
: '',
topic_id: () => topicId ?? '',
topic_title: () => {
if (!topicId) return '';
const topic = topicSelectors.getTopicById(topicId)(getChatStoreState());
return topic?.title ?? '';
},
},
// Extended contexts - only pass when enabled
...(isAgentBuilderEnabled && { agentBuilderContext }),
...(isGroupAgentBuilderEnabled && { groupAgentBuilderContext }),
...(agentManagementContext && { agentManagementContext }),
...(agentGroup && { agentGroup }),
...(planTodoConfig && { planTodo: planTodoConfig }),
...(topicReferences && topicReferences.length > 0 && { topicReferences }),
...(onboardingContext && { onboardingContext }),
});
log('Input messages count: %d', messages.length);
const result = await engine.process();
log('Output messages count: %d', result.messages.length);
if (messages.length > 0 && result.messages.length === 0) {
log(
'WARNING: Messages were reduced to 0! Input messages: %o',
messages.map((m) => ({ id: m.id, role: m.role })),
);
}
return result.messages;
};