rename: handler -> agent framework; plugin -> tool

rename: handler -> agent framework; plugin -> tool

lint

refactor: more rename

further rename
This commit is contained in:
lin onetwo 2025-11-27 15:33:30 +08:00 committed by GitHub
parent 8963527b41
commit 8a84d9b468
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 1382 additions and 1368 deletions

View file

@ -46,7 +46,7 @@ Object.defineProperty(window, 'observables', {
userInfo$: new BehaviorSubject(undefined).asObservable(),
},
agentInstance: {
concatPrompt: vi.fn((promptDescription: Pick<AgentPromptDescription, 'handlerConfig'>, messages: AgentInstanceMessage[]) => {
concatPrompt: vi.fn((promptDescription: Pick<AgentPromptDescription, 'agentFrameworkConfig'>, messages: AgentInstanceMessage[]) => {
const agentInstanceService = container.get<AgentInstanceService>(serviceIdentifier.AgentInstance);
// Initialize handlers (plugins and built-in handlers) before calling concatPrompt
// We need to wrap this in an Observable since concatPrompt returns an Observable
@ -55,7 +55,7 @@ Object.defineProperty(window, 'observables', {
try {
// Need to register plugins first. In test environment, this needs to be called manually. While in real
// environment, this is handled in `main.ts` when app start.
await agentInstanceService.initializeHandlers();
await agentInstanceService.initializeFrameworks();
const resultObservable = agentInstanceService.concatPrompt(promptDescription, messages);
// Subscribe to the result and forward to our observer
resultObservable.subscribe(observer);

View file

@ -4,7 +4,7 @@ import { Box, Button, Container, Step, StepLabel, Stepper, TextField, Typography
import { styled } from '@mui/material/styles';
import type { RJSFSchema } from '@rjsf/utils';
import type { AgentDefinition } from '@services/agentDefinition/interface';
import { HandlerConfig } from '@services/agentInstance/promptConcat/promptConcatSchema';
import { AgentFrameworkConfig } from '@services/agentInstance/promptConcat/promptConcatSchema';
import useDebouncedCallback from 'beautiful-react-hooks/useDebouncedCallback';
import { nanoid } from 'nanoid';
import React, { useEffect, useState } from 'react';
@ -99,19 +99,19 @@ export const CreateNewAgentContent: React.FC<CreateNewAgentContentProps> = ({ ta
// Load schema when temporaryAgentDefinition is available
useEffect(() => {
const loadSchema = async () => {
if (temporaryAgentDefinition?.handlerID) {
if (temporaryAgentDefinition?.agentFrameworkID) {
try {
const schema = await window.service.agentInstance.getHandlerConfigSchema(temporaryAgentDefinition.handlerID);
const schema = await window.service.agentInstance.getFrameworkConfigSchema(temporaryAgentDefinition.agentFrameworkID);
setPromptSchema(schema as RJSFSchema);
} catch (error) {
console.error('Failed to load handler config schema:', error);
console.error('Failed to load framework config schema:', error);
setPromptSchema(null);
}
}
};
void loadSchema();
}, [temporaryAgentDefinition?.handlerID]);
}, [temporaryAgentDefinition?.agentFrameworkID]);
// Create preview agent when entering step 3
useEffect(() => {
@ -374,11 +374,11 @@ export const CreateNewAgentContent: React.FC<CreateNewAgentContentProps> = ({ ta
<Box sx={{ mt: 2, height: 400, overflow: 'auto' }}>
<PromptConfigForm
schema={promptSchema}
formData={(temporaryAgentDefinition.handlerConfig || {}) as HandlerConfig}
formData={(temporaryAgentDefinition.agentFrameworkConfig || {}) as AgentFrameworkConfig}
onChange={(updatedConfig) => {
void handleAgentDefinitionChange({
...temporaryAgentDefinition,
handlerConfig: updatedConfig as Record<string, unknown>,
agentFrameworkConfig: updatedConfig as Record<string, unknown>,
});
}}
loading={false}

View file

@ -2,7 +2,7 @@ import { Box, Button, CircularProgress, Container, Divider, TextField, Typograph
import { styled } from '@mui/material/styles';
import type { RJSFSchema } from '@rjsf/utils';
import type { AgentDefinition } from '@services/agentDefinition/interface';
import { HandlerConfig } from '@services/agentInstance/promptConcat/promptConcatSchema';
import { AgentFrameworkConfig } from '@services/agentInstance/promptConcat/promptConcatSchema';
import useDebouncedCallback from 'beautiful-react-hooks/useDebouncedCallback';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -94,30 +94,30 @@ export const EditAgentDefinitionContent: React.FC<EditAgentDefinitionContentProp
void loadAgentDefinition();
}, [tab.agentDefId]);
// Load handler config schema
// Load framework config schema
useEffect(() => {
const loadSchema = async () => {
if (!agentDefinition?.handlerID) {
// No handlerID found
if (!agentDefinition?.agentFrameworkID) {
// No agentFrameworkID found
return;
}
try {
// Loading handler config schema
const schema = await window.service.agentInstance.getHandlerConfigSchema(agentDefinition.handlerID);
// Loading framework config schema
const schema = await window.service.agentInstance.getFrameworkConfigSchema(agentDefinition.agentFrameworkID);
// Schema loaded successfully
setPromptSchema(schema);
} catch (error) {
void window.service.native.log('error', 'EditAgentDefinitionContent: Failed to load handler config schema', {
void window.service.native.log('error', 'EditAgentDefinitionContent: Failed to load framework config schema', {
error,
handlerID: agentDefinition.handlerID,
agentFrameworkID: agentDefinition.agentFrameworkID,
});
console.error('Failed to load handler config schema:', error);
console.error('Failed to load framework config schema:', error);
}
};
void loadSchema();
}, [agentDefinition?.handlerID]);
}, [agentDefinition?.agentFrameworkID]);
// Auto-save to backend whenever agentDefinition changes (debounced)
const saveToBackendDebounced = useDebouncedCallback(
@ -235,7 +235,7 @@ export const EditAgentDefinitionContent: React.FC<EditAgentDefinitionContentProp
return {
...previous,
handlerConfig: formData as Record<string, unknown>,
agentFrameworkConfig: formData as Record<string, unknown>,
};
},
);
@ -356,7 +356,7 @@ export const EditAgentDefinitionContent: React.FC<EditAgentDefinitionContentProp
<Box sx={{ mt: 2 }} data-testid='edit-agent-prompt-form'>
<PromptConfigForm
schema={promptSchema}
formData={agentDefinition.handlerConfig as HandlerConfig}
formData={agentDefinition.agentFrameworkConfig as AgentFrameworkConfig}
onChange={handlePromptConfigChange}
/>
</Box>

View file

@ -86,7 +86,7 @@ export const NewTabContent: React.FC<NewTabContentProps> = ({ tab: _tab }) => {
const createAgentChatTab = async (agentDefinitionId?: string) => {
try {
const agentDefinitionIdToUse = agentDefinitionId || 'example-agent';
const agentDefinitionIdToUse = agentDefinitionId || 'task-agent';
// Handle current active tab - close temp tabs or NEW_TAB type tabs
if (activeTabId) {
@ -162,7 +162,7 @@ export const NewTabContent: React.FC<NewTabContentProps> = ({ tab: _tab }) => {
const handleEditDefinition = useCallback(() => {
// Use the example agent ID for now - in the future this could be configurable
void editAgentDefinitionTab('example-agent');
void editAgentDefinitionTab('task-agent');
handleCloseContextMenu();
}, []);

View file

@ -17,7 +17,7 @@ const mockGetAgentDefs = vi.fn();
const mockUpdateTab = vi.fn();
const mockGetAllTabs = vi.fn();
const mockGetActiveTabId = vi.fn();
const mockGetHandlerConfigSchema = vi.fn();
const mockGetFrameworkConfigSchema = vi.fn();
Object.defineProperty(window, 'service', {
writable: true,
@ -30,7 +30,7 @@ Object.defineProperty(window, 'service', {
getAgentDefs: mockGetAgentDefs,
},
agentInstance: {
getHandlerConfigSchema: mockGetHandlerConfigSchema,
getFrameworkConfigSchema: mockGetFrameworkConfigSchema,
},
agentBrowser: {
updateTab: mockUpdateTab,
@ -117,7 +117,7 @@ describe('CreateNewAgentContent', () => {
mockUpdateTab.mockResolvedValue(undefined);
mockGetAllTabs.mockResolvedValue([]);
mockGetActiveTabId.mockResolvedValue('test-tab-123');
mockGetHandlerConfigSchema.mockResolvedValue({
mockGetFrameworkConfigSchema.mockResolvedValue({
type: 'object',
properties: {
prompts: {
@ -157,7 +157,7 @@ describe('CreateNewAgentContent', () => {
id: 'template-1',
name: 'Test Template',
description: 'Test Description',
handlerConfig: { systemPrompt: 'Test prompt' },
agentFrameworkConfig: { systemPrompt: 'Test prompt' },
};
mockCreateAgentDef.mockResolvedValue({
@ -258,8 +258,8 @@ describe('CreateNewAgentContent', () => {
const mockAgentDefinition = {
id: 'temp-123',
name: 'Test Agent',
handlerID: 'test-handler',
handlerConfig: { prompts: [{ text: 'Original prompt', role: 'system' }] },
agentFrameworkID: 'test-handler',
agentFrameworkConfig: { prompts: [{ text: 'Original prompt', role: 'system' }] },
};
mockGetAgentDef.mockResolvedValue(mockAgentDefinition);
@ -285,13 +285,13 @@ describe('CreateNewAgentContent', () => {
expect(mockUpdateAgentDef).not.toHaveBeenCalled();
});
it('should trigger schema loading when temporaryAgentDefinition has handlerID', async () => {
// Mock agent definition with handlerID that will be restored
it('should trigger schema loading when temporaryAgentDefinition has agentFrameworkID', async () => {
// Mock agent definition with agentFrameworkID that will be restored
const mockAgentDefinition = {
id: 'temp-123',
name: 'Test Agent',
handlerID: 'test-handler',
handlerConfig: { prompts: [{ text: 'Test prompt', role: 'system' }] },
agentFrameworkID: 'test-handler',
agentFrameworkConfig: { prompts: [{ text: 'Test prompt', role: 'system' }] },
};
mockGetAgentDef.mockResolvedValue(mockAgentDefinition);
@ -313,9 +313,9 @@ describe('CreateNewAgentContent', () => {
expect(mockGetAgentDef).toHaveBeenCalledWith('temp-123');
}, { timeout: 1000 });
// After restoration, the component should have the handlerID and trigger schema loading
// After restoration, the component should have the agentFrameworkID and trigger schema loading
await waitFor(() => {
expect(mockGetHandlerConfigSchema).toHaveBeenCalledWith('test-handler');
expect(mockGetFrameworkConfigSchema).toHaveBeenCalledWith('test-handler');
}, { timeout: 2000 });
});
@ -341,8 +341,8 @@ describe('CreateNewAgentContent', () => {
const mockTemplate = {
id: 'template-1',
name: 'Test Template',
handlerID: 'test-handler',
handlerConfig: { prompts: [{ text: 'Test prompt', role: 'system' }] },
agentFrameworkID: 'test-handler',
agentFrameworkConfig: { prompts: [{ text: 'Test prompt', role: 'system' }] },
};
const mockCreatedDefinition = {
@ -378,8 +378,8 @@ describe('CreateNewAgentContent', () => {
const mockTemplate = {
id: 'template-1',
name: 'Test Template',
handlerID: 'test-handler',
handlerConfig: { prompts: [{ text: 'Original prompt' }] },
agentFrameworkID: 'test-handler',
agentFrameworkConfig: { prompts: [{ text: 'Original prompt' }] },
};
const mockCreatedDefinition = {
@ -443,7 +443,7 @@ describe('CreateNewAgentContent', () => {
expect(mockCreateAgentDef).toHaveBeenCalledWith(
expect.objectContaining({
name: 'My Agent',
handlerID: 'test-handler',
agentFrameworkID: 'test-handler',
}),
);
});
@ -459,19 +459,19 @@ describe('CreateNewAgentContent', () => {
expect(mockUpdateAgentDef).toHaveBeenCalledWith(
expect.objectContaining({
id: expect.stringContaining('temp-'),
handlerID: 'test-handler',
agentFrameworkID: 'test-handler',
}),
);
}, { timeout: 500 });
});
it('should handle nested prompt structure like defaultAgents.json', async () => {
// This is the actual structure from defaultAgents.json
it('should handle nested prompt structure like taskAgents.json', async () => {
// This is the actual structure from taskAgents.json
const mockTemplate = {
id: 'example-agent',
id: 'task-agent',
name: 'Example Agent',
handlerID: 'basicPromptConcatHandler',
handlerConfig: {
agentFrameworkID: 'basicPromptConcatHandler',
agentFrameworkConfig: {
prompts: [
{
id: 'system',
@ -503,7 +503,7 @@ describe('CreateNewAgentContent', () => {
// Step 1: Create agent definition (simulates template selection)
const createdDef = await window.service.agentDefinition.createAgentDef(mockCreatedDefinition);
expect(createdDef).toBeDefined();
const prompts = (createdDef.handlerConfig).prompts as Array<{
const prompts = (createdDef.agentFrameworkConfig).prompts as Array<{
children?: Array<{ text?: string }>;
}>;
expect((prompts as Array<{ children?: Array<{ text?: string }> }>)[0]?.children?.[0]?.text).toBe('You are a helpful assistant for Tiddlywiki user.');
@ -511,14 +511,14 @@ describe('CreateNewAgentContent', () => {
// Step 2: Update system prompt in nested structure
const updatedDefinition = {
...mockCreatedDefinition,
handlerConfig: {
...mockCreatedDefinition.handlerConfig,
agentFrameworkConfig: {
...mockCreatedDefinition.agentFrameworkConfig,
prompts: [
{
...mockCreatedDefinition.handlerConfig.prompts[0],
...mockCreatedDefinition.agentFrameworkConfig.prompts[0],
children: [
{
...mockCreatedDefinition.handlerConfig.prompts[0].children[0],
...mockCreatedDefinition.agentFrameworkConfig.prompts[0].children[0],
text: '你是一个专业的代码助手,请用中文回答编程问题。',
},
],
@ -532,7 +532,7 @@ describe('CreateNewAgentContent', () => {
// Verify the correct nested structure is updated
expect(mockUpdateAgentDef).toHaveBeenCalledWith(
expect.objectContaining({
handlerConfig: expect.objectContaining({
agentFrameworkConfig: expect.objectContaining({
prompts: expect.arrayContaining([
expect.objectContaining({
role: 'system',

View file

@ -17,7 +17,7 @@ const mockCreateAgent = vi.fn();
const mockDeleteAgent = vi.fn();
const mockGetAgentDef = vi.fn();
const mockUpdateAgentDef = vi.fn();
const mockGetHandlerConfigSchema = vi.fn();
const mockGetFrameworkConfigSchema = vi.fn();
const mockLog = vi.fn();
Object.defineProperty(window, 'service', {
@ -33,7 +33,7 @@ Object.defineProperty(window, 'service', {
agentInstance: {
createAgent: mockCreateAgent,
deleteAgent: mockDeleteAgent,
getHandlerConfigSchema: mockGetHandlerConfigSchema,
getFrameworkConfigSchema: mockGetFrameworkConfigSchema,
},
agentDefinition: {
getAgentDef: mockGetAgentDef,
@ -49,7 +49,7 @@ const mockAgentDefinition = {
id: 'test-agent-def-id',
name: 'Test Agent',
description: 'A test agent for editing',
handlerID: 'testHandler',
agentFrameworkID: 'testHandler',
config: {},
};
@ -69,7 +69,7 @@ describe('EditAgentDefinitionContent', () => {
mockGetAllTabs.mockResolvedValue([]);
mockGetActiveTabId.mockResolvedValue(null);
mockGetAgentDef.mockResolvedValue(mockAgentDefinition);
mockGetHandlerConfigSchema.mockResolvedValue(mockSchema);
mockGetFrameworkConfigSchema.mockResolvedValue(mockSchema);
mockCreateAgent.mockResolvedValue({
id: 'test-agent-id',
name: 'Test Agent',
@ -234,7 +234,7 @@ describe('EditAgentDefinitionContent', () => {
await renderComponent();
await waitFor(() => {
expect(mockGetHandlerConfigSchema).toHaveBeenCalledWith('testHandler');
expect(mockGetFrameworkConfigSchema).toHaveBeenCalledWith('testHandler');
});
await waitFor(() => {
@ -277,7 +277,7 @@ describe('EditAgentDefinitionContent', () => {
await renderComponent();
await waitFor(() => {
expect(mockGetHandlerConfigSchema).toHaveBeenCalledWith('testHandler');
expect(mockGetFrameworkConfigSchema).toHaveBeenCalledWith('testHandler');
});
});

View file

@ -41,7 +41,7 @@ describe('NewTabContent', () => {
mockCreateAgent.mockResolvedValue({
id: 'test-agent-id',
name: 'Test Agent',
agentDefId: 'example-agent',
agentDefId: 'task-agent',
});
mockAddTab.mockResolvedValue({
id: 'test-tab-id',

View file

@ -302,35 +302,35 @@ export const agentActions = (
}
},
getHandlerId: async () => {
getAgentFrameworkId: async () => {
try {
const { agent, agentDef } = get();
if (agentDef?.handlerID) {
return agentDef.handlerID;
if (agentDef?.agentFrameworkID) {
return agentDef.agentFrameworkID;
}
if (agent?.agentDefId) {
const fetchedAgentDefinition = await window.service.agentDefinition.getAgentDef(agent.agentDefId);
if (fetchedAgentDefinition?.handlerID) {
return fetchedAgentDefinition.handlerID;
if (fetchedAgentDefinition?.agentFrameworkID) {
return fetchedAgentDefinition.agentFrameworkID;
}
}
throw new Error('No active agent in store or handler ID not found');
throw new Error('No active agent in store or agent framework ID not found');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const finalError = new Error(`Failed to get handler ID: ${errorMessage}`);
const finalError = new Error(`Failed to get agent framework ID: ${errorMessage}`);
set({ error: finalError });
throw finalError;
}
},
/**
* Get handler configuration schema for current handler
* Get framework configuration schema for current framework
*/
getHandlerConfigSchema: async () => {
getFrameworkConfigSchema: async () => {
try {
const handlerId = await get().getHandlerId();
return await window.service.agentInstance.getHandlerConfigSchema(handlerId);
const agentFrameworkId = await get().getAgentFrameworkId();
return await window.service.agentInstance.getFrameworkConfigSchema(agentFrameworkId);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const finalError = new Error(`Failed to get handler schema: ${errorMessage}`);

View file

@ -70,14 +70,14 @@ export const previewActionsMiddleware: StateCreator<AgentChatStoreType, [], [],
getPreviewPromptResult: async (
inputText: string,
handlerConfig: AgentPromptDescription['handlerConfig'],
agentFrameworkConfig: AgentPromptDescription['agentFrameworkConfig'],
) => {
try {
set({ previewLoading: true });
const messages = Array.from(get().messages.values());
// Safety check - if handlerConfig is empty, fail early
if (Object.keys(handlerConfig).length === 0) {
// Safety check - if agentFrameworkConfig is empty, fail early
if (!agentFrameworkConfig || Object.keys(agentFrameworkConfig).length === 0) {
set({ previewLoading: false, previewResult: null });
return null;
}
@ -93,7 +93,7 @@ export const previewActionsMiddleware: StateCreator<AgentChatStoreType, [], [],
}
// Use the streaming API with progress updates
const concatStream = window.observables.agentInstance.concatPrompt({ handlerConfig }, messages);
const concatStream = window.observables.agentInstance.concatPrompt({ agentFrameworkConfig }, messages);
// Initialize progress
set({
@ -113,7 +113,7 @@ export const previewActionsMiddleware: StateCreator<AgentChatStoreType, [], [],
next: (state) => {
// Update progress and current step
const stepDescription = state.step === 'plugin'
? `Processing plugin: ${state.currentPlugin?.pluginId || 'unknown'}`
? `Processing tool: ${state.currentPlugin?.toolId || 'unknown'}`
: state.step === 'finalize'
? 'Finalizing prompts...'
: state.step === 'flatten'
@ -123,7 +123,7 @@ export const previewActionsMiddleware: StateCreator<AgentChatStoreType, [], [],
set({
previewProgress: state.progress,
previewCurrentStep: stepDescription,
previewCurrentPlugin: state.currentPlugin?.pluginId || null,
previewCurrentPlugin: state.currentPlugin?.toolId || null,
// Update intermediate results
previewResult: {
flatPrompts: state.flatPrompts,

View file

@ -90,10 +90,10 @@ export interface BasicActions {
cancelAgent: () => Promise<void>;
/** Get the handler ID for the current agent */
getHandlerId: () => Promise<string>;
getAgentFrameworkId: () => Promise<string>;
/** Get the configuration schema for the current handler */
getHandlerConfigSchema: () => Promise<Record<string, unknown>>;
getFrameworkConfigSchema: () => Promise<Record<string, unknown>>;
/** Process raw agent data into store format */
processAgentData: (
@ -188,12 +188,12 @@ export interface PreviewActions {
/**
* Generates a preview of prompts for the current agent state
* @param inputText Input text to include in the preview
* @param handlerConfig Prompt configuration to use for preview
* @param agentFrameworkConfig Framework configuration to use for preview
* @returns Promise that resolves when preview is generated and state is updated
*/
getPreviewPromptResult: (
inputText: string,
handlerConfig: AgentPromptDescription['handlerConfig'],
agentFrameworkConfig: AgentPromptDescription['agentFrameworkConfig'],
) => Promise<
{
flatPrompts: ModelMessage[];

View file

@ -1,4 +1,4 @@
import { useHandlerConfigManagement } from '@/windows/Preferences/sections/ExternalAPI/useHandlerConfigManagement';
import { useAgentFrameworkConfigManagement } from '@/windows/Preferences/sections/ExternalAPI/useAgentFrameworkConfigManagement';
import MonacoEditor from '@monaco-editor/react';
import { Box, styled } from '@mui/material';
import Tab from '@mui/material/Tab';
@ -9,7 +9,7 @@ import React, { FC, SyntheticEvent, useCallback, useEffect, useState } from 'rea
import { useTranslation } from 'react-i18next';
import { useShallow } from 'zustand/react/shallow';
import { HandlerConfig } from '@services/agentInstance/promptConcat/promptConcatSchema';
import { AgentFrameworkConfig } from '@services/agentInstance/promptConcat/promptConcatSchema';
import { useAgentChatStore } from '../../../Agent/store/agentChatStore/index';
import { PromptConfigForm } from './PromptConfigForm';
@ -40,11 +40,11 @@ export const EditView: FC<EditViewProps> = ({
);
const {
loading: handlerConfigLoading,
config: handlerConfig,
loading: agentFrameworkConfigLoading,
config: agentFrameworkConfig,
schema: handlerSchema,
handleConfigChange,
} = useHandlerConfigManagement({
} = useAgentFrameworkConfigManagement({
agentDefId: agent?.agentDefId,
agentId: agent?.id,
});
@ -98,7 +98,7 @@ export const EditView: FC<EditViewProps> = ({
);
const handleFormChange = useDebouncedCallback(
async (updatedConfig: HandlerConfig) => {
async (updatedConfig: AgentFrameworkConfig) => {
try {
// Ensure the config change is fully persisted before proceeding
await handleConfigChange(updatedConfig);
@ -121,7 +121,7 @@ export const EditView: FC<EditViewProps> = ({
const handleEditorChange = useCallback((value: string | undefined) => {
if (!value) return;
try {
const parsedConfig = JSON.parse(value) as HandlerConfig;
const parsedConfig = JSON.parse(value) as AgentFrameworkConfig;
void handleFormChange(parsedConfig);
} catch (error) {
void window.service.native.log('error', 'EditView: Invalid JSON in code editor:', { error });
@ -163,16 +163,16 @@ export const EditView: FC<EditViewProps> = ({
{editorMode === 'form' && (
<PromptConfigForm
schema={handlerSchema ?? {}}
formData={handlerConfig}
formData={agentFrameworkConfig}
onChange={handleFormChange}
loading={handlerConfigLoading}
loading={agentFrameworkConfigLoading}
/>
)}
{editorMode === 'code' && (
<MonacoEditor
height='100%'
defaultLanguage='json'
value={handlerConfig ? JSON.stringify(handlerConfig, null, 2) : '{}'}
value={agentFrameworkConfig ? JSON.stringify(agentFrameworkConfig, null, 2) : '{}'}
onChange={handleEditorChange}
options={{
minimap: { enabled: true },

View file

@ -3,7 +3,7 @@ import { IChangeEvent } from '@rjsf/core';
import Form from '@rjsf/mui';
import { ObjectFieldTemplateProps, RJSFSchema, RJSFValidationError } from '@rjsf/utils';
import validator from '@rjsf/validator-ajv8';
import { HandlerConfig } from '@services/agentInstance/promptConcat/promptConcatSchema';
import { AgentFrameworkConfig } from '@services/agentInstance/promptConcat/promptConcatSchema';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ErrorDisplay } from './components/ErrorDisplay';
@ -37,9 +37,9 @@ interface PromptConfigFormProps {
/** UI schema for layout customization */
uiSchema?: Record<string, unknown>;
/** Initial form data */
formData?: HandlerConfig;
formData?: AgentFrameworkConfig;
/** Change handler for form data */
onChange?: (formData: HandlerConfig) => void;
onChange?: (formData: AgentFrameworkConfig) => void;
/** Error handler for form validation errors */
onError?: (errors: RJSFValidationError[]) => void;
/** Whether the form is disabled */
@ -87,7 +87,7 @@ export const PromptConfigForm: React.FC<PromptConfigFormProps> = ({
onError?.(errors);
}, [onError]);
const handleChange = useCallback((changeEvent: IChangeEvent<HandlerConfig>) => {
const handleChange = useCallback((changeEvent: IChangeEvent<AgentFrameworkConfig>) => {
const formData = changeEvent.formData;
if (formData) {
onChange?.(formData);

View file

@ -9,16 +9,16 @@ import { ThemeProvider } from '@mui/material/styles';
import { lightTheme } from '@services/theme/defaultTheme';
import { useAgentChatStore } from '@/pages/Agent/store/agentChatStore/index';
import defaultAgents from '@services/agentInstance/buildInAgentHandlers/defaultAgents.json';
import defaultAgents from '@services/agentInstance/agentFrameworks/taskAgents.json';
import { IPrompt } from '@services/agentInstance/promptConcat/promptConcatSchema';
import { ModelMessage } from 'ai';
import { PromptPreviewDialog } from '../index';
// Mock handler config management hook
vi.mock('@/windows/Preferences/sections/ExternalAPI/useHandlerConfigManagement', () => ({
useHandlerConfigManagement: vi.fn(() => ({
vi.mock('@/windows/Preferences/sections/ExternalAPI/useAgentFrameworkConfigManagement', () => ({
useAgentFrameworkConfigManagement: vi.fn(() => ({
loading: false,
config: defaultAgents[0].handlerConfig,
config: defaultAgents[0].agentFrameworkConfig,
handleConfigChange: vi.fn(),
})),
}));
@ -36,7 +36,7 @@ describe('PromptPreviewDialog - Tool Information Rendering', () => {
useAgentChatStore.setState({
agent: {
id: 'test-agent',
agentDefId: 'example-agent',
agentDefId: 'task-agent',
status: { state: 'working', modified: new Date() },
created: new Date(),
},
@ -63,8 +63,8 @@ describe('PromptPreviewDialog - Tool Information Rendering', () => {
// Test if the real concatPrompt is working
expect(globalThis.window?.observables?.agentInstance?.concatPrompt).toBeDefined();
// Create test data matching defaultAgents.json - cast to avoid type issues in test
const handlerConfig = defaultAgents[0].handlerConfig as never;
// Create test data matching taskAgents.json - cast to avoid type issues in test
const agentFrameworkConfig = defaultAgents[0].agentFrameworkConfig as never;
const messages = [{
id: 'test-message-1',
agentId: 'test-agent',
@ -74,7 +74,7 @@ describe('PromptPreviewDialog - Tool Information Rendering', () => {
// Call the real concatPrompt implementation
const observable = globalThis.window.observables.agentInstance.concatPrompt(
{ handlerConfig },
{ agentFrameworkConfig },
messages,
);
@ -118,11 +118,11 @@ describe('PromptPreviewDialog - Tool Information Rendering', () => {
it('should render workspaces and tools info from real concatPrompt execution', async () => {
// First execute real concatPrompt to get the structured data
const handlerConfig = defaultAgents[0].handlerConfig;
const agentFrameworkConfig = defaultAgents[0].agentFrameworkConfig;
const messages = [{ id: 'test', role: 'user' as const, content: 'Hello world', created: new Date(), modified: new Date(), agentId: 'test' }];
// Pass handlerConfig wrapped (same shape used elsewhere)
const observable = window.observables.agentInstance.concatPrompt({ handlerConfig } as never, messages);
// Pass agentFrameworkConfig wrapped (same shape used elsewhere)
const observable = window.observables.agentInstance.concatPrompt({ agentFrameworkConfig } as never, messages);
const results: unknown[] = [];
let finalResult: { flatPrompts: ModelMessage[]; processedPrompts: IPrompt[] } | undefined;

View file

@ -9,14 +9,14 @@ import { ThemeProvider } from '@mui/material/styles';
import { lightTheme } from '@services/theme/defaultTheme';
import { useAgentChatStore } from '@/pages/Agent/store/agentChatStore/index';
import defaultAgents from '@services/agentInstance/buildInAgentHandlers/defaultAgents.json';
import defaultAgents from '@services/agentInstance/agentFrameworks/taskAgents.json';
import { PromptPreviewDialog } from '../index';
// Mock handler config management hook
vi.mock('@/windows/Preferences/sections/ExternalAPI/useHandlerConfigManagement', () => ({
useHandlerConfigManagement: vi.fn(() => ({
vi.mock('@/windows/Preferences/sections/ExternalAPI/useAgentFrameworkConfigManagement', () => ({
useAgentFrameworkConfigManagement: vi.fn(() => ({
loading: false,
config: defaultAgents[0].handlerConfig,
config: defaultAgents[0].agentFrameworkConfig,
handleConfigChange: vi.fn(),
})),
}));

View file

@ -1,4 +1,4 @@
import { useHandlerConfigManagement } from '@/windows/Preferences/sections/ExternalAPI/useHandlerConfigManagement';
import { useAgentFrameworkConfigManagement } from '@/windows/Preferences/sections/ExternalAPI/useAgentFrameworkConfigManagement';
import CloseIcon from '@mui/icons-material/Close';
import EditIcon from '@mui/icons-material/Edit';
import FullscreenIcon from '@mui/icons-material/Fullscreen';
@ -36,9 +36,9 @@ export const PromptPreviewDialog: React.FC<PromptPreviewDialogProps> = ({
const [isEditMode, setIsEditMode] = useState(false);
const {
loading: handlerConfigLoading,
config: handlerConfig,
} = useHandlerConfigManagement({
loading: agentFrameworkConfigLoading,
config: agentFrameworkConfig,
} = useAgentFrameworkConfigManagement({
agentDefId: agent?.agentDefId,
agentId: agent?.id,
});
@ -54,17 +54,17 @@ export const PromptPreviewDialog: React.FC<PromptPreviewDialogProps> = ({
);
useEffect(() => {
const fetchInitialPreview = async () => {
if (!agent?.agentDefId || handlerConfigLoading || !handlerConfig || !open) {
if (!agent?.agentDefId || agentFrameworkConfigLoading || !agentFrameworkConfig || !open) {
return;
}
try {
await getPreviewPromptResult(inputText, handlerConfig);
await getPreviewPromptResult(inputText, agentFrameworkConfig);
} catch (error) {
console.error('PromptPreviewDialog: Error fetching initial preview:', error);
}
};
void fetchInitialPreview();
}, [agent?.agentDefId, handlerConfig, handlerConfigLoading, inputText, open]); // 移除 getPreviewPromptResult
}, [agent?.agentDefId, agentFrameworkConfig, agentFrameworkConfigLoading, inputText, open]); // 移除 getPreviewPromptResult
const handleToggleFullScreen = useCallback((): void => {
setIsFullScreen(previous => !previous);

View file

@ -1,6 +1,6 @@
import { AgentDefinitionService } from '@services/agentDefinition';
import { AgentDefinition } from '@services/agentDefinition/interface';
import defaultAgents from '@services/agentInstance/buildInAgentHandlers/defaultAgents.json';
import defaultAgents from '@services/agentInstance/agentFrameworks/taskAgents.json';
import type { IAgentInstanceService } from '@services/agentInstance/interface';
import { container } from '@services/container';
import type { IDatabaseService } from '@services/database/interface';
@ -90,12 +90,13 @@ describe('AgentDefinitionService getAgentDefs integration', () => {
const defs = await freshService.getAgentDefs();
expect(defs.length).toBeGreaterThan(0);
const exampleAgent = defs.find(d => d.id === (defaultAgents as AgentDefinition[])[0].id);
// Fixed
const exampleAgent = defs.find(d => d.id === (defaultAgents as unknown as AgentDefinition[])[0].id);
expect(exampleAgent).toBeDefined();
expect(exampleAgent!.name).toBeDefined();
expect(exampleAgent!.handlerID).toBeDefined();
expect(exampleAgent!.handlerConfig).toBeDefined();
expect(typeof exampleAgent!.handlerConfig).toBe('object');
expect(exampleAgent!.agentFrameworkID).toBeDefined();
expect(exampleAgent!.agentFrameworkConfig).toBeDefined();
expect(typeof exampleAgent!.agentFrameworkConfig).toBe('object');
});
it('should return only database data without fallback to defaultAgents', async () => {
@ -105,7 +106,7 @@ describe('AgentDefinitionService getAgentDefs integration', () => {
const agentDefRepo = realDataSource.getRepository(AgentDefinitionEntity);
// Save only minimal record (id only) to test new behavior
const example = (defaultAgents as AgentDefinition[])[0];
const example = (defaultAgents as unknown as AgentDefinition[])[0];
await agentDefRepo.save({
id: example.id,
});
@ -116,11 +117,11 @@ describe('AgentDefinitionService getAgentDefs integration', () => {
expect(found).toBeDefined();
// With new behavior, only id should be present, other fields should be undefined or empty
expect(found!.id).toBe(example.id);
expect(found!.handlerID).toBeUndefined();
expect(found!.agentFrameworkID).toBeUndefined();
expect(found!.name).toBeUndefined();
expect(found!.description).toBeUndefined();
expect(found!.avatarUrl).toBeUndefined();
expect(found!.handlerConfig).toEqual({});
expect(found!.agentFrameworkConfig).toEqual({});
expect(found!.aiApiConfig).toBeUndefined();
expect(found!.agentTools).toBeUndefined();
});
@ -132,7 +133,7 @@ describe('AgentDefinitionService getAgentDefs integration', () => {
const agentDefRepo = realDataSource.getRepository(AgentDefinitionEntity);
// Save only minimal record (id only) as per new behavior
const example = (defaultAgents as AgentDefinition[])[0];
const example = (defaultAgents as unknown as AgentDefinition[])[0];
await agentDefRepo.save({
id: example.id,
});
@ -148,8 +149,8 @@ describe('AgentDefinitionService getAgentDefs integration', () => {
expect(entity!.name).toBeNull();
expect(entity!.description).toBeNull();
expect(entity!.avatarUrl).toBeNull();
expect(entity!.handlerID).toBeNull();
expect(entity!.handlerConfig).toBeNull();
expect(entity!.agentFrameworkID).toBeNull();
expect(entity!.agentFrameworkConfig).toBeNull();
expect(entity!.aiApiConfig).toBeNull();
expect(entity!.agentTools).toBeNull();
});
@ -158,15 +159,15 @@ describe('AgentDefinitionService getAgentDefs integration', () => {
const templates = await agentDefinitionService.getAgentTemplates();
// Should include all default agents
expect(templates.length).toBe((defaultAgents as AgentDefinition[]).length);
expect(templates.length).toBe((defaultAgents as unknown as AgentDefinition[]).length);
// Check that template has complete data from defaultAgents.json
const exampleTemplate = templates.find(t => t.id === (defaultAgents as AgentDefinition[])[0].id);
// Check that template has complete data from taskAgents.json
const exampleTemplate = templates.find(t => t.id === (defaultAgents as unknown as AgentDefinition[])[0].id);
expect(exampleTemplate).toBeDefined();
expect(exampleTemplate!.name).toBeDefined();
expect(exampleTemplate!.handlerID).toBeDefined();
expect(exampleTemplate!.handlerConfig).toBeDefined();
expect(typeof exampleTemplate!.handlerConfig).toBe('object');
expect(exampleTemplate!.agentFrameworkID).toBeDefined();
expect(exampleTemplate!.agentFrameworkConfig).toBeDefined();
expect(typeof exampleTemplate!.agentFrameworkConfig).toBe('object');
});
it('should not throw when searchName filtering is requested (client-side filtering expected)', async () => {
@ -185,6 +186,6 @@ describe('AgentDefinitionService getAgentDefs integration', () => {
// Should still return default agents and not throw
const templates = await agentDefinitionService.getAgentTemplates();
expect(templates.length).toBe((defaultAgents as AgentDefinition[]).length);
expect(templates.length).toBe((defaultAgents as unknown as AgentDefinition[]).length);
});
});

View file

@ -116,21 +116,21 @@ export function validateAndConvertWikiTiddlerToAgentTemplate(
};
// Try to parse the tiddler text as JSON for agent configuration
let handlerConfig: Record<string, unknown>;
let agentFrameworkConfig: Record<string, unknown>;
try {
const textContent = typeof tiddler.text === 'string' ? tiddler.text : JSON.stringify(tiddler.text || '{}');
const parsed = JSON.parse(textContent) as unknown;
// Ensure handlerConfig is a valid object
// Ensure agentFrameworkConfig is a valid object
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
logger.warn('Invalid handlerConfig in tiddler', {
logger.warn('Invalid agentFrameworkConfig in tiddler', {
function: 'validateAndConvertWikiTiddlerToAgentTemplate',
title: getStringField(tiddler.title),
reason: 'not an object',
});
return null;
}
handlerConfig = parsed as Record<string, unknown>;
agentFrameworkConfig = parsed as Record<string, unknown>;
} catch (parseError) {
logger.warn('Failed to parse agent template from tiddler', {
function: 'validateAndConvertWikiTiddlerToAgentTemplate',
@ -146,8 +146,8 @@ export function validateAndConvertWikiTiddlerToAgentTemplate(
name: getStringField(tiddler.caption) || getStringField(tiddler.title),
description: getStringField(tiddler.description) || `Agent template from ${workspaceName || 'wiki'}`,
avatarUrl: getStringField(tiddler.avatar_url) || undefined,
handlerID: getStringField(tiddler.handler_id) || 'basicPromptConcatHandler',
handlerConfig,
agentFrameworkID: getStringField(tiddler.agentFrameworkID) || 'basicPromptConcatHandler',
agentFrameworkConfig,
aiApiConfig: parseAiApiConfig(tiddler.ai_api_config),
agentTools: parseAgentTools(tiddler.agent_tools),
};

View file

@ -4,7 +4,7 @@ import { nanoid } from 'nanoid';
import { DataSource, Repository } from 'typeorm';
import type { IAgentBrowserService } from '@services/agentBrowser/interface';
import defaultAgents from '@services/agentInstance/buildInAgentHandlers/defaultAgents.json';
import defaultAgents from '@services/agentInstance/agentFrameworks/taskAgents.json';
import type { IAgentInstanceService } from '@services/agentInstance/interface';
import { container } from '@services/container';
import type { IDatabaseService } from '@services/database/interface';
@ -71,15 +71,15 @@ export class AgentDefinitionService implements IAgentDefinitionService {
if (existingCount === 0) {
logger.info('Agent database is empty, initializing with default agents');
const defaultAgentsList = defaultAgents as AgentDefinition[];
// Create agent definition entities with complete data from defaultAgents.json
// Create agent definition entities with complete data from taskAgents.json
const agentDefinitionEntities = defaultAgentsList.map(defaultAgent =>
this.agentDefRepository!.create({
id: defaultAgent.id,
name: defaultAgent.name,
description: defaultAgent.description,
avatarUrl: defaultAgent.avatarUrl,
handlerID: defaultAgent.handlerID,
handlerConfig: defaultAgent.handlerConfig,
agentFrameworkID: defaultAgent.agentFrameworkID,
agentFrameworkConfig: defaultAgent.agentFrameworkConfig,
aiApiConfig: defaultAgent.aiApiConfig,
agentTools: defaultAgent.agentTools,
})
@ -143,7 +143,7 @@ export class AgentDefinitionService implements IAgentDefinitionService {
throw new Error(`Agent definition not found: ${agent.id}`);
}
const pickedProperties = pick(agent, ['name', 'description', 'avatarUrl', 'handlerID', 'handlerConfig', 'aiApiConfig']);
const pickedProperties = pick(agent, ['name', 'description', 'avatarUrl', 'agentFrameworkID', 'agentFrameworkConfig', 'aiApiConfig']);
Object.assign(existingAgent, pickedProperties);
await this.agentDefRepository!.save(existingAgent);
@ -171,8 +171,8 @@ export class AgentDefinitionService implements IAgentDefinitionService {
name: entity.name || undefined,
description: entity.description || undefined,
avatarUrl: entity.avatarUrl || undefined,
handlerID: entity.handlerID || undefined,
handlerConfig: entity.handlerConfig || {},
agentFrameworkID: entity.agentFrameworkID || undefined,
agentFrameworkConfig: entity.agentFrameworkConfig || {},
aiApiConfig: entity.aiApiConfig || undefined,
agentTools: entity.agentTools || undefined,
}));
@ -212,8 +212,8 @@ export class AgentDefinitionService implements IAgentDefinitionService {
name: entity.name || undefined,
description: entity.description || undefined,
avatarUrl: entity.avatarUrl || undefined,
handlerID: entity.handlerID || undefined,
handlerConfig: entity.handlerConfig || {},
agentFrameworkID: entity.agentFrameworkID || undefined,
agentFrameworkConfig: entity.agentFrameworkConfig || {},
aiApiConfig: entity.aiApiConfig || undefined,
agentTools: entity.agentTools || undefined,
};

View file

@ -42,10 +42,10 @@ export interface AgentDefinition {
description?: string;
/** Agent icon or avatar URL */
avatarUrl?: string;
/** Agent handler function's id, we will find function by this id */
handlerID?: string;
/** Agent handler's config, specific to the handler. This is required to ensure agent has valid configuration. */
handlerConfig: Record<string, unknown>;
/** Agent framework function's id, we will find function by this id */
agentFrameworkID?: string;
/** Agent framework's config, specific to the framework. This is required to ensure agent has valid configuration. */
agentFrameworkConfig: Record<string, unknown>;
/**
* Overwrite the default AI configuration for this agent.
* Priority is higher than the global default agent config.

View file

@ -6,7 +6,7 @@ import type { IExternalAPIService } from '@services/externalAPI/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AgentInstanceService } from '..';
import { basicPromptConcatHandler } from '../buildInAgentHandlers/basicPromptConcatHandler';
import { basicPromptConcatHandler } from '../agentFrameworks/taskAgent';
import type { AgentInstanceMessage, IAgentInstanceService } from '../interface';
import type { AiAPIConfig } from '../promptConcat/promptConcatSchema';
@ -78,7 +78,7 @@ describe('AgentInstance failure path - external API logs on error', () => {
agentDef: {
id: 'def-1',
name: 'Def 1',
handlerConfig: {},
agentFrameworkConfig: {},
aiApiConfig: { api: { provider: 'test-provider', model: 'test-model' }, modelParameters: { temperature: 0.7, systemPrompt: '', topP: 0.95 } },
},
isCancelled: () => false,

View file

@ -12,7 +12,7 @@ import { container } from '@services/container';
import type { IDatabaseService } from '@services/database/interface';
import type { IExternalAPIService } from '@services/externalAPI/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import defaultAgents from '../buildInAgentHandlers/defaultAgents.json';
import defaultAgents from '../agentFrameworks/taskAgents.json';
describe('AgentInstanceService Streaming Behavior', () => {
let agentInstanceService: IAgentInstanceService;
@ -55,7 +55,7 @@ describe('AgentInstanceService Streaming Behavior', () => {
agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
await agentInstanceService.initialize();
// Setup test agent instance using data from defaultAgents.json
// Setup test agent instance using data from taskAgents.json
const exampleAgent = defaultAgents[0];
testAgentInstance = {
id: nanoid(),
@ -73,7 +73,7 @@ describe('AgentInstanceService Streaming Behavior', () => {
// Mock agent definition service to return our test agent definition
mockAgentDefinitionService.getAgentDef = vi.fn().mockResolvedValue({
...exampleAgent,
handlerID: 'basicPromptConcatHandler',
agentFrameworkID: 'basicPromptConcatHandler',
});
// Mock the getAgent method to return our test instance
vi.spyOn(agentInstanceService, 'getAgent').mockResolvedValue(testAgentInstance);

View file

@ -7,7 +7,7 @@ import serviceIdentifier from '@services/serviceIdentifier';
import type { IWikiService } from '@services/wiki/interface';
import { nanoid } from 'nanoid';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import defaultAgents from '../buildInAgentHandlers/defaultAgents.json';
import defaultAgents from '../agentFrameworks/taskAgents.json';
// Follow structure of index.streaming.test.ts
describe('AgentInstanceService Wiki Operation', () => {
@ -26,9 +26,9 @@ describe('AgentInstanceService Wiki Operation', () => {
agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
// Initialize both database repositories and handlers
await agentInstanceService.initializeHandlers();
await agentInstanceService.initializeFrameworks();
// Setup test agent instance using data from defaultAgents.json
// Setup test agent instance using data from taskAgents.json
const exampleAgent = defaultAgents[0];
testAgentInstance = {
id: nanoid(),

View file

@ -2,11 +2,11 @@ import { describe, expect, it } from 'vitest';
import { createAgentInstanceData } from '../utilities';
describe('createAgentInstanceData', () => {
it('should create agent instance with undefined handlerConfig (fallback to definition)', () => {
it('should create agent instance with undefined agentFrameworkConfig (fallback to definition)', () => {
const agentDefinition = {
id: 'test-agent-def',
name: 'Test Agent',
handlerConfig: {
agentFrameworkConfig: {
prompts: [
{
text: 'You are a helpful assistant.',
@ -14,29 +14,29 @@ describe('createAgentInstanceData', () => {
},
],
},
handlerID: 'basicPromptConcatHandler',
agentFrameworkID: 'basicPromptConcatHandler',
};
const { instanceData } = createAgentInstanceData(agentDefinition);
expect(instanceData.handlerConfig).toBeUndefined();
expect(instanceData.agentFrameworkConfig).toBeUndefined();
expect(instanceData.agentDefId).toBe('test-agent-def');
expect(instanceData.handlerID).toBe('basicPromptConcatHandler');
expect(instanceData.agentFrameworkID).toBe('basicPromptConcatHandler');
expect(instanceData.name).toContain('Test Agent');
});
it('should create agent instance with undefined handlerConfig even when definition has required handlerConfig', () => {
it('should create agent instance with undefined agentFrameworkConfig even when definition has required agentFrameworkConfig', () => {
const agentDefinition = {
id: 'test-agent-def-no-config',
name: 'Test Agent No Config',
handlerID: 'basicPromptConcatHandler',
handlerConfig: {}, // Required by AgentDefinition interface
agentFrameworkID: 'basicPromptConcatHandler',
agentFrameworkConfig: {}, // Required by AgentDefinition interface
};
const { instanceData } = createAgentInstanceData(agentDefinition);
expect(instanceData.handlerConfig).toBeUndefined();
expect(instanceData.agentFrameworkConfig).toBeUndefined();
expect(instanceData.agentDefId).toBe('test-agent-def-no-config');
expect(instanceData.handlerID).toBe('basicPromptConcatHandler');
expect(instanceData.agentFrameworkID).toBe('basicPromptConcatHandler');
});
});

View file

@ -8,8 +8,8 @@ import serviceIdentifier from '@services/serviceIdentifier';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { AgentInstanceMessage, IAgentInstanceService } from '../../interface';
import type { AiAPIConfig } from '../../promptConcat/promptConcatSchema';
import { basicPromptConcatHandler } from '../basicPromptConcatHandler';
import type { AgentHandlerContext } from '../type';
import { basicPromptConcatHandler } from '../taskAgent';
import type { AgentFrameworkContext } from '../utilities/type';
// Use real normalizeRole implementation — do not mock plugins or persistence in these integration tests
@ -21,7 +21,7 @@ const mockErrorDetail = {
message: 'Invalid prompt: message must be a ModelMessage or a UI message',
};
function makeContext(agentId: string, agentDefId: string, messages: AgentInstanceMessage[]): AgentHandlerContext {
function makeContext(agentId: string, agentDefId: string, messages: AgentInstanceMessage[]): AgentFrameworkContext {
return {
agent: {
id: agentId,
@ -33,11 +33,11 @@ function makeContext(agentId: string, agentDefId: string, messages: AgentInstanc
agentDef: {
id: agentDefId,
name: 'Test Agent',
handlerConfig: {},
agentFrameworkConfig: {},
aiApiConfig: { api: { provider: 'test-provider', model: 'test-model' }, modelParameters: { temperature: 0.7, systemPrompt: '', topP: 0.95 } } as AiAPIConfig,
},
isCancelled: () => false,
} as unknown as AgentHandlerContext;
} as unknown as AgentFrameworkContext;
}
describe('basicPromptConcatHandler - failure path persists error message and logs', () => {
@ -94,10 +94,10 @@ describe('basicPromptConcatHandler - failure path persists error message and log
vi.spyOn(agentDefSvc, 'getAgentDef').mockResolvedValue({
id: 'def-1',
name: 'Def 1',
handlerID: 'basicPromptConcatHandler',
handlerConfig: {
agentFrameworkID: 'basicPromptConcatHandler',
agentFrameworkConfig: {
plugins: [
{ pluginId: 'wikiOperation', wikiOperationParam: {} },
{ toolId: 'wikiOperation', wikiOperationParam: {} },
],
},
});

View file

@ -18,7 +18,7 @@ import { WikiChannel } from '@/constants/channels';
// types are provided by shared mock; no local type assertions needed
// Import defaultAgents configuration
import defaultAgents from '../defaultAgents.json';
import defaultAgents from '../taskAgents.json';
// Configurable test hooks for mocks
let testWikiImplementation: ((channel: WikiChannel, workspaceId?: string, args?: string[]) => Promise<unknown>) | undefined;
@ -27,12 +27,12 @@ let testStreamResponses: Array<{ status: string; content: string; requestId: str
// Use real AgentInstanceService in tests; do not mock
// Import plugin components for direct testing
import type { IPromptConcatPlugin } from '@services/agentInstance/promptConcat/promptConcatSchema';
import type { IPromptConcatTool } from '@services/agentInstance/promptConcat/promptConcatSchema';
import type { IDatabaseService } from '@services/database/interface';
import { createHandlerHooks, createHooksWithPlugins, initializePluginSystem, PromptConcatHookContext } from '../../plugins/index';
import { wikiSearchPlugin } from '../../plugins/wikiSearchPlugin';
import { basicPromptConcatHandler } from '../basicPromptConcatHandler';
import type { AgentHandlerContext } from '../type';
import { createAgentFrameworkHooks, createHooksWithTools, initializeToolSystem, PromptConcatHookContext } from '../../tools/index';
import { wikiSearchTool } from '../../tools/wikiSearch';
import { basicPromptConcatHandler } from '../taskAgent';
import type { AgentFrameworkContext } from '../utilities/type';
describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
beforeEach(async () => {
@ -41,8 +41,8 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
testStreamResponses = [];
const { container } = await import('@services/container');
// Ensure built-in plugin registry includes all built-in plugins
await initializePluginSystem();
// Ensure built-in tool registry includes all built-in tools
await initializeToolSystem();
// Prepare a mock DataSource/repository so AgentInstanceService.initialize() can run
const mockRepo = {
@ -88,32 +88,32 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
describe('Complete Workflow Integration', () => {
it('should complete full wiki search workflow: tool list -> tool execution -> response', async () => {
// Use real agent config from defaultAgents.json
// Use real agent config from taskAgents.json
const exampleAgent = defaultAgents[0];
const handlerConfig = exampleAgent.handlerConfig;
const agentFrameworkConfig = exampleAgent.agentFrameworkConfig;
// Get the wiki search plugin configuration
const wikiPlugin = handlerConfig.plugins.find(p => p.pluginId === 'wikiSearch');
// Get the wiki search tool configuration
const wikiPlugin = agentFrameworkConfig.plugins.find(p => p.toolId === 'wikiSearch');
expect(wikiPlugin).toBeDefined();
if (!wikiPlugin) throw new Error('wikiPlugin not found');
const prompts = JSON.parse(JSON.stringify(handlerConfig.prompts));
const prompts = JSON.parse(JSON.stringify(agentFrameworkConfig.prompts));
// Phase 1: Tool List Injection
const promptConcatHookContext: PromptConcatHookContext = {
handlerContext: {
agentFrameworkContext: {
agent: {
id: 'test-agent',
messages: [],
agentDefId: exampleAgent.id,
status: { state: 'working' as const, modified: new Date() },
created: new Date(),
handlerConfig: {},
agentFrameworkConfig: {},
},
agentDef: { id: exampleAgent.id, name: exampleAgent.name, handlerConfig: exampleAgent.handlerConfig },
agentDef: { id: exampleAgent.id, name: exampleAgent.name, agentFrameworkConfig: exampleAgent.agentFrameworkConfig },
isCancelled: () => false,
},
pluginConfig: wikiPlugin as IPromptConcatPlugin,
toolConfig: wikiPlugin as IPromptConcatTool,
prompts,
messages: [
{
@ -127,12 +127,12 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
],
};
// Create hooks and register plugins as defined in handlerConfig
const { hooks: promptHooks } = await createHooksWithPlugins(handlerConfig);
// First run workspacesList plugin to inject available workspaces (if present)
const workspacesPlugin = handlerConfig.plugins?.find(p => p.pluginId === 'workspacesList');
// Create hooks and register tools as defined in agentFrameworkConfig
const { hooks: promptHooks } = await createHooksWithTools(agentFrameworkConfig);
// First run workspacesList tool to inject available workspaces (if present)
const workspacesPlugin = agentFrameworkConfig.plugins?.find(p => p.toolId === 'workspacesList');
if (workspacesPlugin) {
const workspacesContext = { ...promptConcatHookContext, pluginConfig: workspacesPlugin } as unknown as PromptConcatHookContext;
const workspacesContext = { ...promptConcatHookContext, toolConfig: workspacesPlugin } as unknown as PromptConcatHookContext;
await promptHooks.processPrompts.promise(workspacesContext);
}
// Then run wikiSearch plugin to inject the tool list
@ -169,7 +169,7 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
};
const responseContext = {
handlerContext: {
agentFrameworkContext: {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -179,9 +179,9 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
},
created: new Date(),
messages: [],
handlerConfig: {},
agentFrameworkConfig: {},
},
agentDef: { id: 'test-agent-def', name: 'test-agent-def', handlerConfig: {} } as AgentDefinition,
agentDef: { id: 'test-agent-def', name: 'test-agent-def', agentFrameworkConfig: {} } as unknown as AgentDefinition,
isCancelled: () => false,
},
response: {
@ -191,7 +191,7 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
},
requestId: 'test-request',
isFinal: true,
pluginConfig: wikiPlugin as IPromptConcatPlugin,
toolConfig: wikiPlugin as IPromptConcatTool,
prompts: [],
messages: [],
llmResponse: 'I will search for important content using wiki-search tool.',
@ -199,8 +199,8 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
actions: {} as unknown as Record<string, unknown>,
};
// Use hooks registered with all plugins from handlerConfig
const { hooks: responseHooks } = await createHooksWithPlugins(handlerConfig);
// Use hooks registered with all plugins import { AgentFrameworkConfig }
const { hooks: responseHooks } = await createHooksWithTools(agentFrameworkConfig);
// Execute the response complete hook
await responseHooks.responseComplete.promise(responseContext);
// reuse containerForAssert from above assertions
@ -213,8 +213,8 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
expect(responseContext.actions.yieldNextRoundTo).toBe('self');
// Verify tool result message was added to agent history
expect(responseContext.handlerContext.agent.messages.length).toBeGreaterThan(0);
const toolResultMessage = responseContext.handlerContext.agent.messages[responseContext.handlerContext.agent.messages.length - 1] as AgentInstanceMessage;
expect(responseContext.agentFrameworkContext.agent.messages.length).toBeGreaterThan(0);
const toolResultMessage = responseContext.agentFrameworkContext.agent.messages[responseContext.agentFrameworkContext.agent.messages.length - 1] as AgentInstanceMessage;
expect(toolResultMessage.role).toBe('tool'); // Tool result message
expect(toolResultMessage.content).toContain('<functions_result>');
expect(toolResultMessage.content).toContain('Tool: wiki-search');
@ -222,18 +222,18 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
});
it('should handle errors in wiki search gracefully', async () => {
// Use real agent config from defaultAgents.json
// Use real agent config from taskAgents.json
const exampleAgent = defaultAgents[0];
const handlerConfig = exampleAgent.handlerConfig;
const agentFrameworkConfig = exampleAgent.agentFrameworkConfig;
// Get the wiki search plugin configuration
const wikiPlugin = handlerConfig.plugins.find(p => p.pluginId === 'wikiSearch');
// Get the wiki search tool configuration
const wikiPlugin = agentFrameworkConfig.plugins.find(p => p.toolId === 'wikiSearch');
expect(wikiPlugin).toBeDefined();
// Mock tool calling with invalid workspace
const responseContext = {
handlerContext: {
agentFrameworkContext: {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -243,9 +243,9 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
},
created: new Date(),
messages: [],
handlerConfig: {},
agentFrameworkConfig: {},
},
agentDef: { id: 'test-agent-def', name: 'test-agent-def', handlerConfig: {} } as AgentDefinition,
agentDef: { id: 'test-agent-def', name: 'test-agent-def', agentFrameworkConfig: {} } as unknown as AgentDefinition,
isCancelled: () => false,
},
response: {
@ -255,7 +255,7 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
},
requestId: 'test-request',
isFinal: true,
pluginConfig: wikiPlugin as IPromptConcatPlugin,
toolConfig: wikiPlugin as IPromptConcatTool,
prompts: [],
messages: [],
llmResponse: 'Search in nonexistent wiki',
@ -264,10 +264,10 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
};
// Use real handler hooks
const responseHooks = createHandlerHooks();
const responseHooks = createAgentFrameworkHooks();
// Register the plugin
wikiSearchPlugin(responseHooks);
wikiSearchTool(responseHooks);
// Execute the response complete hook
await responseHooks.responseComplete.promise(responseContext);
@ -276,8 +276,8 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
expect(responseContext.actions.yieldNextRoundTo).toBe('self');
// Verify error message was added to agent history
expect(responseContext.handlerContext.agent.messages.length).toBeGreaterThan(0);
const errorResultMessage = responseContext.handlerContext.agent.messages[responseContext.handlerContext.agent.messages.length - 1] as AgentInstanceMessage;
expect(responseContext.agentFrameworkContext.agent.messages.length).toBeGreaterThan(0);
const errorResultMessage = responseContext.agentFrameworkContext.agent.messages[responseContext.agentFrameworkContext.agent.messages.length - 1] as AgentInstanceMessage;
expect(errorResultMessage.role).toBe('tool'); // Tool error message
// The error should be indicated in the message content
@ -295,7 +295,7 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
const exampleAgent = defaultAgents[0];
const testAgentId = `test-agent-${Date.now()}`;
const context: AgentHandlerContext = {
const context: AgentFrameworkContext = {
agent: {
id: testAgentId,
agentDefId: exampleAgent.id,
@ -315,7 +315,7 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
agentDef: {
id: exampleAgent.id,
name: exampleAgent.name,
handlerConfig: exampleAgent.handlerConfig,
agentFrameworkConfig: exampleAgent.agentFrameworkConfig,
},
isCancelled: () => false,
};

View file

@ -4,14 +4,14 @@ import { logger } from '@services/libs/log';
import serviceIdentifier from '@services/serviceIdentifier';
import { merge } from 'lodash';
import type { AgentInstanceLatestStatus, AgentInstanceMessage, IAgentInstanceService } from '../interface';
import { createHooksWithPlugins } from '../plugins';
import { YieldNextRoundTarget } from '../plugins/types';
import { AgentPromptDescription, AiAPIConfig, HandlerConfig } from '../promptConcat/promptConcatSchema';
import type { IPromptConcatPlugin } from '../promptConcat/promptConcatSchema/plugin';
import { AgentFrameworkConfig, AgentPromptDescription, AiAPIConfig } from '../promptConcat/promptConcatSchema';
import type { IPromptConcatTool } from '../promptConcat/promptConcatSchema/plugin';
import { responseConcat } from '../promptConcat/responseConcat';
import { getFinalPromptResult } from '../promptConcat/utilities';
import { canceled, completed, error, working } from './statusUtilities';
import { AgentHandlerContext } from './type';
import { createHooksWithTools } from '../tools';
import { YieldNextRoundTarget } from '../tools/types';
import { canceled, completed, error, working } from './utilities/statusUtilities';
import { AgentFrameworkContext } from './utilities/type';
/**
* Main conversation orchestrator for AI agents
@ -27,19 +27,19 @@ import { AgentHandlerContext } from './type';
*
* @param context - Agent handling context containing configuration and message history
*/
export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
export async function* basicPromptConcatHandler(context: AgentFrameworkContext) {
// Initialize variables for request tracking
let currentRequestId: string | undefined;
const lastUserMessage: AgentInstanceMessage | undefined = context.agent.messages[context.agent.messages.length - 1];
// Create and register handler hooks based on handler config
const { hooks: handlerHooks, pluginConfigs } = await createHooksWithPlugins(context.agentDef.handlerConfig || {});
// Create and register handler hooks based on framework config
const { hooks: agentFrameworkHooks, toolConfigs } = await createHooksWithTools(context.agentDef.agentFrameworkConfig || {});
// Log the start of handler execution with context information
logger.debug('Starting prompt handler execution', {
method: 'basicPromptConcatHandler',
agentId: context.agent.id,
defId: context.agentDef.id,
handlerId: context.agentDef.handlerID,
agentFrameworkId: context.agentDef.agentFrameworkID,
messageCount: context.agent.messages.length,
});
// Check if there's a new user message to process - trigger user message received hook
@ -48,8 +48,8 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
if (isNewUserMessage) {
// Trigger user message received hook
await handlerHooks.userMessageReceived.promise({
handlerContext: context,
await agentFrameworkHooks.userMessageReceived.promise({
agentFrameworkContext: context,
content: {
text: lastUserMessage.content,
file: lastUserMessage.metadata?.file as File | undefined,
@ -62,8 +62,8 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
lastUserMessage.metadata = { ...lastUserMessage.metadata, processed: true };
// Trigger agent status change to working
await handlerHooks.agentStatusChanged.promise({
handlerContext: context,
await agentFrameworkHooks.agentStatusChanged.promise({
agentFrameworkContext: context,
status: {
state: 'working',
modified: new Date(),
@ -94,12 +94,12 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
// Process prompts using common handler function
try {
const handlerConfig: HandlerConfig = context.agentDef.handlerConfig as HandlerConfig;
const agentFrameworkConfig = context.agentDef.agentFrameworkConfig as AgentFrameworkConfig;
const agentPromptDescription: AgentPromptDescription = {
id: context.agentDef.id,
api: aiApiConfig.api,
modelParameters: aiApiConfig.modelParameters,
handlerConfig,
agentFrameworkConfig,
};
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
@ -146,12 +146,12 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
if (response.status === 'update') {
// For responseUpdate, we'll skip plugin-specific config for now
// since it's called frequently during streaming
await handlerHooks.responseUpdate.promise({
handlerContext: context,
await agentFrameworkHooks.responseUpdate.promise({
agentFrameworkContext: context,
response,
requestId: currentRequestId,
isFinal: false,
pluginConfig: {} as IPromptConcatPlugin, // Empty config for streaming updates
toolConfig: {} as IPromptConcatTool, // Empty config for streaming updates
});
}
@ -164,16 +164,16 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
// Delegate final response processing to handler hooks
const responseCompleteContext = {
handlerContext: context,
agentFrameworkContext: context,
response,
requestId: currentRequestId,
isFinal: true,
pluginConfig: (pluginConfigs.length > 0 ? pluginConfigs[0] : {}) as IPromptConcatPlugin, // First config for compatibility
handlerConfig: context.agentDef.handlerConfig, // Pass complete config for plugin access
toolConfig: (toolConfigs.length > 0 ? toolConfigs[0] : {}) as IPromptConcatTool, // First config for compatibility
agentFrameworkConfig: context.agentDef.agentFrameworkConfig, // Pass complete config for tool access
actions: undefined as { yieldNextRoundTo?: 'self' | 'human'; newUserMessage?: string } | undefined,
};
await handlerHooks.responseComplete.promise(responseCompleteContext);
await agentFrameworkHooks.responseComplete.promise(responseCompleteContext);
// Check if responseComplete hooks set yieldNextRoundTo
let yieldNextRoundFromHooks: YieldNextRoundTarget | undefined;

View file

@ -1,11 +1,11 @@
[
{
"id": "example-agent",
"name": "Example Agent",
"id": "task-agent",
"name": "Task Agent",
"description": "Example agent with prompt processing",
"avatarUrl": "https://example.com/example-agent.png",
"handlerID": "basicPromptConcatHandler",
"handlerConfig": {
"avatarUrl": "https://example.com/task-agent.png",
"agentFrameworkID": "basicPromptConcatHandler",
"agentFrameworkConfig": {
"prompts": [
{
"id": "system",
@ -72,7 +72,7 @@
"plugins": [
{
"id": "efe5be74-540d-487d-8a05-7377e486953d",
"pluginId": "fullReplacement",
"toolId": "fullReplacement",
"fullReplacementParam": {
"targetId": "default-history",
"sourceType": "historyOfSession"
@ -84,7 +84,7 @@
"id": "f0e1b2c3-4d5e-6f7g-8h9i-j0k1l2m3n4o5",
"caption": "Wiki工作空间列表",
"description": "自动在提示词中注入可用的Wiki工作空间列表",
"pluginId": "workspacesList",
"toolId": "workspacesList",
"workspacesListParam": {
"targetId": "default-before-tool",
"position": "after"
@ -94,7 +94,7 @@
"id": "d0f1b2c3-4d5e-6f7g-8h9i-j0k1l2m3n4o5",
"caption": "Wiki搜索和向量索引工具",
"description": "提供Wiki搜索filter和vector以及向量嵌入索引管理功能",
"pluginId": "wikiSearch",
"toolId": "wikiSearch",
"wikiSearchParam": {
"sourceType": "wiki",
"toolListPosition": {
@ -107,7 +107,7 @@
"id": "e1f2b3c4-5d6e-7f8g-9h0i-k1l2m3n4o5p6",
"caption": "Wiki操作工具",
"description": "允许AI在Wiki工作空间中创建、更新和删除笔记",
"pluginId": "wikiOperation",
"toolId": "wikiOperation",
"wikiOperationParam": {
"toolListPosition": {
"position": "after",
@ -117,7 +117,7 @@
},
{
"id": "a0f1b2c3-4d5e-6f7g-8h9i-j0k1l2m3n4o5",
"pluginId": "fullReplacement",
"toolId": "fullReplacement",
"fullReplacementParam": {
"targetId": "default-response",
"sourceType": "llmResponse"

View file

@ -3,8 +3,8 @@
*/
import { nanoid } from 'nanoid';
import { AgentInstanceLatestStatus } from '../interface';
import { AgentHandlerContext } from './type';
import { AgentInstanceLatestStatus } from '../../interface';
import { AgentFrameworkContext } from './type';
/**
* Creates a completed status with error information in message metadata
@ -22,7 +22,7 @@ export function completedWithError(
provider: string;
message?: string;
} | undefined,
context: AgentHandlerContext,
context: AgentFrameworkContext,
messageId?: string,
): AgentInstanceLatestStatus {
return {

View file

@ -1,6 +1,6 @@
import { nanoid } from 'nanoid';
import { AgentInstanceLatestStatus } from '../interface';
import { AgentHandlerContext } from './type';
import { AgentInstanceLatestStatus } from '../../interface';
import { AgentFrameworkContext } from './type';
/**
* Creates a working status with a message
@ -11,7 +11,7 @@ import { AgentHandlerContext } from './type';
*/
export function working(
content: string,
context: AgentHandlerContext,
context: AgentFrameworkContext,
messageId?: string,
): AgentInstanceLatestStatus {
return {
@ -34,7 +34,7 @@ export function working(
*/
export function completed(
content: string,
context: AgentHandlerContext,
context: AgentFrameworkContext,
messageId?: string,
): AgentInstanceLatestStatus {
return {
@ -72,7 +72,7 @@ export function error(
provider: string;
message?: string;
} | undefined,
context: AgentHandlerContext,
context: AgentFrameworkContext,
messageId?: string,
): AgentInstanceLatestStatus {
return {

View file

@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-invalid-void-type */
import { AgentDefinition } from '../../agentDefinition/interface';
import { AgentInstance, AgentInstanceLatestStatus } from '../interface';
import { AgentDefinition } from '../../../agentDefinition/interface';
import { AgentInstance, AgentInstanceLatestStatus } from '../../interface';
export interface AgentHandlerContext {
export interface AgentFrameworkContext {
agent: AgentInstance;
agentDef: AgentDefinition;
@ -28,6 +28,6 @@ export interface AgentHandlerContext {
* (needed for non-streaming 'tasks/send'). If void is returned, the server uses the
* last known state from the store after processing all yields.
*/
export type AgentHandler = (
context: AgentHandlerContext,
export type AgentFramework = (
context: AgentFrameworkContext,
) => AsyncGenerator<AgentInstanceLatestStatus, AgentInstance | undefined | void, unknown>;

View file

@ -5,12 +5,12 @@ import { BehaviorSubject, Observable } from 'rxjs';
import { DataSource, Repository } from 'typeorm';
import type { IAgentDefinitionService } from '@services/agentDefinition/interface';
import { basicPromptConcatHandler } from '@services/agentInstance/buildInAgentHandlers/basicPromptConcatHandler';
import type { AgentHandler, AgentHandlerContext } from '@services/agentInstance/buildInAgentHandlers/type';
import { createHooksWithPlugins, initializePluginSystem } from '@services/agentInstance/plugins';
import { basicPromptConcatHandler } from '@services/agentInstance/agentFrameworks/taskAgent';
import type { AgentFramework, AgentFrameworkContext } from '@services/agentInstance/agentFrameworks/utilities/type';
import { promptConcatStream, PromptConcatStreamState } from '@services/agentInstance/promptConcat/promptConcat';
import type { AgentPromptDescription } from '@services/agentInstance/promptConcat/promptConcatSchema';
import { getPromptConcatHandlerConfigJsonSchema } from '@services/agentInstance/promptConcat/promptConcatSchema/jsonSchema';
import { getPromptConcatAgentFrameworkConfigJsonSchema } from '@services/agentInstance/promptConcat/promptConcatSchema/jsonSchema';
import { createHooksWithTools, initializeToolSystem } from '@services/agentInstance/tools';
import type { IDatabaseService } from '@services/database/interface';
import { AgentInstanceEntity, AgentInstanceMessageEntity } from '@services/database/schema/agent';
import { logger } from '@services/libs/log';
@ -34,15 +34,15 @@ export class AgentInstanceService implements IAgentInstanceService {
private agentInstanceSubjects: Map<string, BehaviorSubject<AgentInstance | undefined>> = new Map();
private statusSubjects: Map<string, BehaviorSubject<AgentInstanceLatestStatus | undefined>> = new Map();
private agentHandlers: Map<string, AgentHandler> = new Map();
private handlerSchemas: Map<string, Record<string, unknown>> = new Map();
private agentFrameworks: Map<string, AgentFramework> = new Map();
private frameworkSchemas: Map<string, Record<string, unknown>> = new Map();
private cancelTokenMap: Map<string, { value: boolean }> = new Map();
private debouncedUpdateFunctions: Map<string, (message: AgentInstanceLatestStatus['message'] & { id: string }, agentId?: string) => void> = new Map();
public async initialize(): Promise<void> {
try {
await this.initializeDatabase();
await this.initializeHandlers();
await this.initializeFrameworks();
} catch (error) {
logger.error('Failed to initialize agent instance service', { error });
throw error;
@ -62,37 +62,37 @@ export class AgentInstanceService implements IAgentInstanceService {
}
}
public async initializeHandlers(): Promise<void> {
public async initializeFrameworks(): Promise<void> {
try {
// Register plugins to global registry once during initialization
await initializePluginSystem();
logger.debug('AgentInstance Plugin system initialized and plugins registered to global registry');
// Register tools to global registry once during initialization
await initializeToolSystem();
logger.debug('AgentInstance Tool system initialized and tools registered to global registry');
// Register built-in handlers
this.registerBuiltinHandlers();
logger.debug('AgentInstance handlers registered');
// Register built-in frameworks
this.registerBuiltinFrameworks();
logger.debug('AgentInstance frameworks registered');
} catch (error) {
logger.error('Failed to initialize agent instance handlers', { error });
logger.error('Failed to initialize agent instance frameworks', { error });
throw error;
}
}
public registerBuiltinHandlers(): void {
// Plugins are already registered in initialize(), so we only register handlers here
// Register basic prompt concatenation handler with its schema
this.registerHandler('basicPromptConcatHandler', basicPromptConcatHandler, getPromptConcatHandlerConfigJsonSchema());
public registerBuiltinFrameworks(): void {
// Tools are already registered in initialize(), so we only register frameworks here
// Register basic prompt concatenation framework with its schema
this.registerFramework('basicPromptConcatHandler', basicPromptConcatHandler, getPromptConcatAgentFrameworkConfigJsonSchema());
}
/**
* Register a handler with an optional schema
* @param handlerId ID for the handler
* @param handler The handler function
* @param schema Optional JSON schema for the handler configuration
* Register a framework with an optional schema
* @param frameworkId ID for the framework
* @param framework The framework function
* @param schema Optional JSON schema for the framework configuration
*/
private registerHandler(handlerId: string, handler: AgentHandler, schema?: Record<string, unknown>): void {
this.agentHandlers.set(handlerId, handler);
private registerFramework(frameworkId: string, framework: AgentFramework, schema?: Record<string, unknown>): void {
this.agentFrameworks.set(frameworkId, framework);
if (schema) {
this.handlerSchemas.set(handlerId, schema);
this.frameworkSchemas.set(frameworkId, schema);
}
}
@ -216,7 +216,7 @@ export class AgentInstanceService implements IAgentInstanceService {
}
// Update fields using pick + Object.assign for consistency with updateAgentDef
const pickedProperties = pick(data, ['name', 'status', 'avatarUrl', 'aiApiConfig', 'closed', 'handlerConfig']);
const pickedProperties = pick(data, ['name', 'status', 'avatarUrl', 'aiApiConfig', 'closed', 'agentFrameworkConfig']);
Object.assign(instanceEntity, pickedProperties);
// Save instance updates
@ -353,20 +353,20 @@ export class AgentInstanceService implements IAgentInstanceService {
throw new Error(`Agent definition not found: ${agentInstance.agentDefId}`);
}
// Get appropriate handler
const handlerId = agentDefinition.handlerID;
if (!handlerId) {
throw new Error(`Handler ID not found in agent definition: ${agentDefinition.id}`);
// Get appropriate framework
const agentFrameworkId = agentDefinition.agentFrameworkID;
if (!agentFrameworkId) {
throw new Error(`Agent framework ID not found in agent definition: ${agentDefinition.id}`);
}
const handler = this.agentHandlers.get(handlerId);
if (!handler) {
throw new Error(`Handler not found: ${handlerId}`);
const framework = this.agentFrameworks.get(agentFrameworkId);
if (!framework) {
throw new Error(`Framework not found: ${agentFrameworkId}`);
}
// Create handler context with temporary message added for processing
// Create framework context with temporary message added for processing
const cancelToken = { value: false };
this.cancelTokenMap.set(agentId, cancelToken);
const handlerContext: AgentHandlerContext = {
const frameworkContext: AgentFrameworkContext = {
agent: {
...agentInstance,
messages: [...agentInstance.messages],
@ -379,23 +379,23 @@ export class AgentInstanceService implements IAgentInstanceService {
isCancelled: () => cancelToken.value,
};
// Create fresh hooks for this handler execution and register plugins based on handlerConfig
const { hooks: handlerHooks } = await createHooksWithPlugins(agentDefinition.handlerConfig || {});
// Create fresh hooks for this framework execution and register tools based on frameworkConfig
const { hooks: frameworkHooks } = await createHooksWithTools(agentDefinition.agentFrameworkConfig || {});
// Trigger userMessageReceived hook with the configured plugins
await handlerHooks.userMessageReceived.promise({
handlerContext,
// Trigger userMessageReceived hook with the configured tools
await frameworkHooks.userMessageReceived.promise({
agentFrameworkContext: frameworkContext,
content,
messageId,
timestamp: now,
});
// Notify agent update after user message is added
this.notifyAgentUpdate(agentId, handlerContext.agent);
this.notifyAgentUpdate(agentId, frameworkContext.agent);
try {
// Create async generator
const generator = handler(handlerContext);
const generator = framework(frameworkContext);
// Track the last message for completion handling
let lastResult: AgentInstanceLatestStatus | undefined;
@ -415,7 +415,7 @@ export class AgentInstanceService implements IAgentInstanceService {
}
// Notify agent update with latest messages for real-time UI updates
this.notifyAgentUpdate(agentId, handlerContext.agent);
this.notifyAgentUpdate(agentId, frameworkContext.agent);
}
// Store the last result for completion handling
@ -442,8 +442,8 @@ export class AgentInstanceService implements IAgentInstanceService {
}
// Trigger agentStatusChanged hook for completion
await handlerHooks.agentStatusChanged.promise({
handlerContext,
await frameworkHooks.agentStatusChanged.promise({
agentFrameworkContext: frameworkContext,
status: {
state: 'completed',
modified: new Date(),
@ -458,8 +458,8 @@ export class AgentInstanceService implements IAgentInstanceService {
logger.error(`Agent handler execution failed: ${errorMessage}`);
// Trigger agentStatusChanged hook for failure
await handlerHooks.agentStatusChanged.promise({
handlerContext,
await frameworkHooks.agentStatusChanged.promise({
agentFrameworkContext: frameworkContext,
status: {
state: 'failed',
modified: new Date(),
@ -848,31 +848,31 @@ export class AgentInstanceService implements IAgentInstanceService {
}
}
public concatPrompt(promptDescription: Pick<AgentPromptDescription, 'handlerConfig'>, messages: AgentInstanceMessage[]): Observable<PromptConcatStreamState> {
public concatPrompt(promptDescription: Pick<AgentPromptDescription, 'agentFrameworkConfig'>, messages: AgentInstanceMessage[]): Observable<PromptConcatStreamState> {
logger.debug('AgentInstanceService.concatPrompt called', {
hasPromptConfig: !!promptDescription.handlerConfig,
promptConfigKeys: Object.keys(promptDescription.handlerConfig),
hasPromptConfig: !!promptDescription.agentFrameworkConfig,
promptConfigKeys: Object.keys(promptDescription.agentFrameworkConfig || {}),
messagesCount: messages.length,
});
return new Observable<PromptConcatStreamState>((observer) => {
const processStream = async () => {
try {
// Create a minimal handler context for prompt concatenation
const handlerContext = {
// Create a minimal framework context for prompt concatenation
const frameworkContext = {
agent: {
id: 'temp',
messages,
agentDefId: 'temp',
status: { state: 'working' as const, modified: new Date() },
created: new Date(),
handlerConfig: {},
agentFrameworkConfig: {},
},
agentDef: { id: 'temp', name: 'temp', handlerConfig: promptDescription.handlerConfig },
agentDef: { id: 'temp', name: 'temp', agentFrameworkConfig: promptDescription.agentFrameworkConfig || {} },
isCancelled: () => false,
};
const streamGenerator = promptConcatStream(promptDescription as AgentPromptDescription, messages, handlerContext);
const streamGenerator = promptConcatStream(promptDescription as AgentPromptDescription, messages, frameworkContext);
for await (const state of streamGenerator) {
observer.next(state);
if (state.isComplete) {
@ -893,21 +893,21 @@ export class AgentInstanceService implements IAgentInstanceService {
});
}
public getHandlerConfigSchema(handlerId: string): Record<string, unknown> {
public getFrameworkConfigSchema(frameworkId: string): Record<string, unknown> {
try {
logger.debug('AgentInstanceService.getHandlerConfigSchema called', { handlerId });
// Check if we have a schema for this handler
const schema = this.handlerSchemas.get(handlerId);
logger.debug('AgentInstanceService.getFrameworkConfigSchema called', { frameworkId });
// Check if we have a schema for this framework
const schema = this.frameworkSchemas.get(frameworkId);
if (schema) {
return schema;
}
// If no schema found, return an empty schema
logger.warn(`No schema found for handler: ${handlerId}`);
logger.warn(`No schema found for framework: ${frameworkId}`);
return { type: 'object', properties: {} };
} catch (error) {
logger.error('Error in AgentInstanceService.getHandlerConfigSchema', {
logger.error('Error in AgentInstanceService.getFrameworkConfigSchema', {
error,
handlerId,
frameworkId,
});
throw error;
}

View file

@ -8,16 +8,16 @@ import { AgentPromptDescription } from '@services/agentInstance/promptConcat/pro
/**
* Content of a session instance that user chat with an agent.
* Inherits from AgentDefinition but makes handlerConfig optional to allow fallback.
* Inherits import { AgentFrameworkConfig } optional to allow fallback.
* The instance can override the definition's configuration, or fall back to using it.
*/
export interface AgentInstance extends Omit<AgentDefinition, 'name' | 'handlerConfig'> {
export interface AgentInstance extends Omit<AgentDefinition, 'name' | 'agentFrameworkConfig'> {
/** Agent description ID that generates this instance */
agentDefId: string;
/** Session name, optional in instance unlike definition */
name?: string;
/** Agent handler's config - optional, falls back to AgentDefinition.handlerConfig if not set */
handlerConfig?: Record<string, unknown>;
/** Agent framework's config - optional, falls back to AgentDefinition.agentFrameworkConfig if not set */
agentFrameworkConfig?: Record<string, unknown>;
/**
* Message history.
* latest on top, so it's easy to get first one as user's latest input, and rest as history.
@ -119,7 +119,7 @@ export interface IAgentInstanceService {
/**
* For testing purposes, only initialize the built-in handlers without database
*/
initializeHandlers(): Promise<void>;
initializeFrameworks(): Promise<void>;
/**
* Create a new agent instance from a definition
@ -196,15 +196,15 @@ export interface IAgentInstanceService {
* @param messages Messages to be included in prompt generation
* @returns Observable stream of processing states, with final state containing complete results
*/
concatPrompt(promptDescription: Pick<AgentPromptDescription, 'handlerConfig'>, messages: AgentInstanceMessage[]): Observable<PromptConcatStreamState>;
concatPrompt(promptDescription: Pick<AgentPromptDescription, 'agentFrameworkConfig'>, messages: AgentInstanceMessage[]): Observable<PromptConcatStreamState>;
/**
* Get JSON Schema for handler configuration
* This allows frontend to generate a form based on the schema for a specific handler
* @param handlerId Handler ID to get schema for
* @param agentFrameworkID Handler ID to get schema for
* @returns JSON Schema for handler configuration
*/
getHandlerConfigSchema(handlerId: string): Record<string, unknown>;
getFrameworkConfigSchema(frameworkId: string): Record<string, unknown>;
/**
* Save user message to database
@ -233,7 +233,7 @@ export const AgentInstanceServiceIPCDescriptor = {
deleteAgent: ProxyPropertyType.Function,
getAgent: ProxyPropertyType.Function,
getAgents: ProxyPropertyType.Function,
getHandlerConfigSchema: ProxyPropertyType.Function,
getFrameworkConfigSchema: ProxyPropertyType.Function,
saveUserMessage: ProxyPropertyType.Function,
sendMsgToAgent: ProxyPropertyType.Function,
subscribeToAgentUpdates: ProxyPropertyType.Function$,

View file

@ -1,188 +0,0 @@
import { logger } from '@services/libs/log';
import { AsyncSeriesHook, AsyncSeriesWaterfallHook } from 'tapable';
import { registerPluginParameterSchema } from './schemaRegistry';
import { AgentResponse, PromptConcatHookContext, PromptConcatHooks, PromptConcatPlugin, ResponseHookContext } from './types';
// Re-export types for convenience
export type { AgentResponse, PromptConcatHookContext, PromptConcatHooks, PromptConcatPlugin, ResponseHookContext };
/**
* Registry for built-in plugins
*/
export const builtInPlugins = new Map<string, PromptConcatPlugin>();
/**
* Create unified hooks instance for the complete plugin system
*/
export function createHandlerHooks(): PromptConcatHooks {
return {
// Prompt processing hooks
processPrompts: new AsyncSeriesWaterfallHook(['context']),
finalizePrompts: new AsyncSeriesWaterfallHook(['context']),
postProcess: new AsyncSeriesWaterfallHook(['context']),
// Agent lifecycle hooks
userMessageReceived: new AsyncSeriesHook(['context']),
agentStatusChanged: new AsyncSeriesHook(['context']),
toolExecuted: new AsyncSeriesHook(['context']),
responseUpdate: new AsyncSeriesHook(['context']),
responseComplete: new AsyncSeriesHook(['context']),
};
}
/**
* Get all available plugins
*/
async function getAllPlugins() {
const [
promptPluginsModule,
wikiSearchModule,
wikiOperationModule,
workspacesListModule,
messageManagementModule,
] = await Promise.all([
import('./promptPlugins'),
import('./wikiSearchPlugin'),
import('./wikiOperationPlugin'),
import('./workspacesListPlugin'),
import('./messageManagementPlugin'),
]);
return {
messageManagementPlugin: messageManagementModule.messageManagementPlugin,
fullReplacementPlugin: promptPluginsModule.fullReplacementPlugin,
wikiSearchPlugin: wikiSearchModule.wikiSearchPlugin,
wikiOperationPlugin: wikiOperationModule.wikiOperationPlugin,
workspacesListPlugin: workspacesListModule.workspacesListPlugin,
};
}
/**
* Register plugins to hooks based on handler configuration
* @param hooks - The hooks instance to register plugins to
* @param handlerConfig - The handler configuration containing plugin settings
*/
export async function registerPluginsToHooksFromConfig(
hooks: PromptConcatHooks,
handlerConfig: { plugins?: Array<{ pluginId: string; [key: string]: unknown }> },
): Promise<void> {
// Always register core plugins that are needed for basic functionality
const messageManagementModule = await import('./messageManagementPlugin');
messageManagementModule.messageManagementPlugin(hooks);
logger.debug('Registered messageManagementPlugin to hooks');
// Register plugins based on handler configuration
if (handlerConfig.plugins) {
for (const pluginConfig of handlerConfig.plugins) {
const { pluginId } = pluginConfig;
// Get plugin from global registry (supports both built-in and dynamic plugins)
const plugin = builtInPlugins.get(pluginId);
if (plugin) {
plugin(hooks);
logger.debug(`Registered plugin ${pluginId} to hooks`);
} else {
logger.warn(`Plugin not found in registry: ${pluginId}`);
}
}
}
}
/**
* Initialize plugin system - register all built-in plugins to global registry
* This should be called once during service initialization
*/
export async function initializePluginSystem(): Promise<void> {
// Import plugin schemas and register them
const [
promptPluginsModule,
wikiSearchModule,
wikiOperationModule,
workspacesListModule,
modelContextProtocolModule,
] = await Promise.all([
import('./promptPlugins'),
import('./wikiSearchPlugin'),
import('./wikiOperationPlugin'),
import('./workspacesListPlugin'),
import('./modelContextProtocolPlugin'),
]);
// Register plugin parameter schemas
registerPluginParameterSchema(
'fullReplacement',
promptPluginsModule.getFullReplacementParameterSchema(),
{
displayName: 'Full Replacement',
description: 'Replace target content with content from specified source',
},
);
registerPluginParameterSchema(
'dynamicPosition',
promptPluginsModule.getDynamicPositionParameterSchema(),
{
displayName: 'Dynamic Position',
description: 'Insert content at a specific position relative to a target element',
},
);
registerPluginParameterSchema(
'wikiSearch',
wikiSearchModule.getWikiSearchParameterSchema(),
{
displayName: 'Wiki Search',
description: 'Search content in wiki workspaces and manage vector embeddings',
},
);
registerPluginParameterSchema(
'wikiOperation',
wikiOperationModule.getWikiOperationParameterSchema(),
{
displayName: 'Wiki Operation',
description: 'Perform operations on wiki workspaces (create, update, delete tiddlers)',
},
);
registerPluginParameterSchema(
'workspacesList',
workspacesListModule.getWorkspacesListParameterSchema(),
{
displayName: 'Workspaces List',
description: 'Inject available wiki workspaces list into prompts',
},
);
registerPluginParameterSchema(
'modelContextProtocol',
modelContextProtocolModule.getModelContextProtocolParameterSchema(),
{
displayName: 'Model Context Protocol',
description: 'MCP (Model Context Protocol) integration',
},
);
const plugins = await getAllPlugins();
// Register all built-in plugins to global registry for discovery
builtInPlugins.set('messageManagement', plugins.messageManagementPlugin);
builtInPlugins.set('fullReplacement', plugins.fullReplacementPlugin);
builtInPlugins.set('wikiSearch', plugins.wikiSearchPlugin);
builtInPlugins.set('wikiOperation', plugins.wikiOperationPlugin);
builtInPlugins.set('workspacesList', plugins.workspacesListPlugin);
logger.debug('All built-in plugins and schemas registered successfully');
}
/**
* Create hooks and register plugins based on handler configuration
* This creates a new hooks instance and registers plugins for that specific context
*/
export async function createHooksWithPlugins(
handlerConfig: { plugins?: Array<{ pluginId: string; [key: string]: unknown }> },
): Promise<{ hooks: PromptConcatHooks; pluginConfigs: Array<{ pluginId: string; [key: string]: unknown }> }> {
const hooks = createHandlerHooks();
await registerPluginsToHooksFromConfig(hooks, handlerConfig);
return {
hooks,
pluginConfigs: handlerConfig.plugins || [],
};
}

View file

@ -1,165 +0,0 @@
/**
* Plugin Schema Registry
*
* This system allows plugins to register their parameter schemas dynamically,
* enabling dynamic plugin loading while maintaining type safety and validation.
*/
import { identity } from 'lodash';
import { z } from 'zod/v4';
const t = identity;
/**
* Registry for plugin parameter schemas
*/
const pluginSchemas = new Map<string, z.ZodType>();
/**
* Registry for plugin metadata
*/
const pluginMetadata = new Map<string, {
displayName: string;
description: string;
}>();
/**
* Register a plugin parameter schema
* @param pluginId The plugin ID (should match pluginId enum values)
* @param schema The Zod schema for this plugin's parameters
* @param metadata Optional metadata for display purposes
*/
export function registerPluginParameterSchema(
pluginId: string,
schema: z.ZodType,
metadata?: {
displayName: string;
description: string;
},
): void {
pluginSchemas.set(pluginId, schema);
if (metadata) {
pluginMetadata.set(pluginId, metadata);
}
}
/**
* Get a plugin parameter schema by ID
* @param pluginId The plugin ID
* @returns The schema or undefined if not found
*/
export function getPluginParameterSchema(pluginId: string): z.ZodType | undefined {
return pluginSchemas.get(pluginId);
}
/**
* Get all registered plugin IDs
* @returns Array of all registered plugin IDs
*/
export function getAllRegisteredPluginIds(): string[] {
return Array.from(pluginSchemas.keys());
}
/**
* Get plugin metadata
* @param pluginId The plugin ID
* @returns Plugin metadata or undefined if not found
*/
export function getPluginMetadata(pluginId: string): { displayName: string; description: string } | undefined {
return pluginMetadata.get(pluginId);
}
/**
* Dynamically create the PromptConcatPluginSchema based on registered plugins
* This is called whenever the schema is needed, ensuring it includes all registered plugins
*/
export function createDynamicPromptConcatPluginSchema(): z.ZodType {
// Base plugin configuration without parameter-specific fields
const basePluginSchema = z.object({
id: z.string().meta({
title: t('Schema.Plugin.IdTitle'),
description: t('Schema.Plugin.Id'),
}),
caption: z.string().optional().meta({
title: t('Schema.Plugin.CaptionTitle'),
description: t('Schema.Plugin.Caption'),
}),
content: z.string().optional().meta({
title: t('Schema.Plugin.ContentTitle'),
description: t('Schema.Plugin.Content'),
}),
forbidOverrides: z.boolean().optional().default(false).meta({
title: t('Schema.Plugin.ForbidOverridesTitle'),
description: t('Schema.Plugin.ForbidOverrides'),
}),
});
// Get all registered plugin IDs
const registeredPluginIds = getAllRegisteredPluginIds();
if (registeredPluginIds.length === 0) {
// Fallback to a basic schema if no plugins are registered yet
return basePluginSchema.extend({
pluginId: z.string().meta({
title: t('Schema.Plugin.PluginIdTitle'),
description: t('Schema.Plugin.PluginId'),
}),
});
}
// Create enum from registered plugin IDs
const pluginIdEnum = z.enum(registeredPluginIds as [string, ...string[]]).meta({
title: t('Schema.Plugin.PluginIdTitle'),
description: t('Schema.Plugin.PluginId'),
enumOptions: registeredPluginIds.map(pluginId => {
const metadata = getPluginMetadata(pluginId);
return {
value: pluginId,
label: metadata?.displayName || pluginId,
};
}),
});
// Create parameter schema object with all registered plugins
const parameterSchema: Record<string, z.ZodType> = {};
for (const pluginId of registeredPluginIds) {
const schema = getPluginParameterSchema(pluginId);
if (schema) {
const metadata = getPluginMetadata(pluginId);
parameterSchema[`${pluginId}Param`] = schema.optional().meta({
title: metadata?.displayName || pluginId,
description: metadata?.description || `Parameters for ${pluginId} plugin`,
});
}
}
// Combine base schema with plugin ID and parameters
return basePluginSchema.extend({
pluginId: pluginIdEnum,
...parameterSchema,
});
}
/**
* Get the type of a plugin's parameters
* @param pluginId The plugin ID
* @returns The inferred TypeScript type of the plugin's parameters
*/
export type PluginParameterType<T extends string> = T extends keyof ReturnType<typeof createPluginParameterTypes> ? ReturnType<typeof createPluginParameterTypes>[T] : never;
/**
* Create type definitions for all registered plugin parameters
* This is used internally for type inference
*/
export function createPluginParameterTypes() {
const types: Record<string, unknown> = {};
for (const pluginId of getAllRegisteredPluginIds()) {
const schema = getPluginParameterSchema(pluginId);
if (schema) {
types[pluginId] = schema;
}
}
return types as Record<string, z.ZodType>;
}

View file

@ -1,54 +1,55 @@
# Prompt Concat Tools
Prompt engineering and message processing with a plugin-based architecture.
Prompt engineering and message processing with a tool-based architecture.
If final prompt is a food, then `handlerConfig.prompts` is the recipe. Chat history and user input are raw materials.
If final prompt is a food, then `agentFrameworkConfig.prompts` is the recipe. Chat history and user input are raw materials.
## Implementation
The `promptConcat` function uses a tapable hooks-based plugin system. Built-in plugins are registered by `pluginId` and loaded based on configuration in `defaultAgents.json`.
The `promptConcat` function uses a tapable hooks-based tool system. Built-in tools are registered by `toolId` and loaded based on configuration in `taskAgents.json`.
### Plugin System Architecture
### Tool System Architecture
1. **Hooks**: Uses tapable `AsyncSeriesWaterfallHook` for plugin execution
1. **Hooks**: Uses tapable `AsyncSeriesWaterfallHook` for tool execution
- `processPrompts`: Modifies prompt tree during processing
- `finalizePrompts`: Final processing before LLM call
- `postProcess`: Handles response processing
2. **Built-in Plugins**:
2. **Built-in Tools**:
- `fullReplacement`: Replaces content from various sources
- `dynamicPosition`: Inserts content at specific positions
- `retrievalAugmentedGeneration`: Retrieves content from wiki/external sources
- `modelContextProtocol`: Integrates with external MCP servers
- `toolCalling`: Processes function calls in responses
3. **Plugin Registration**:
- Plugins are registered by `pluginId` field in the `plugins` array
- Each plugin instance has its own configuration parameters
- Built-in plugins are auto-registered on system initialization
3. **Tool Registration**:
- Tools are registered by `toolId` field in the `plugins` array
- Each tool instance has its own configuration parameters
- Built-in tools are auto-registered on system initialization
### Plugin Lifecycle
### Tool Lifecycle
2. **Configuration**: Plugins are loaded based on `handlerConfig.plugins` array
3. **Execution**: Hooks execute plugins in registration order
4. **Error Handling**: Individual plugin failures don't stop the pipeline
1. **Registration**: Tools are registered during initialization
2. **Configuration**: Tools are loaded based on `agentFrameworkConfig.plugins` array
3. **Execution**: Hooks execute tools in registration order
4. **Error Handling**: Individual tool failures don't stop the pipeline
### Adding New Plugins
### Adding New Tools
1. Create plugin function in `plugins/` directory
2. Register in `plugins/index.ts`
3. Add `pluginId` to schema enum
1. Create tool function in `tools/` directory
2. Register in `tools/index.ts`
3. Add `toolId` to schema enum
4. Add parameter schema if needed
Each plugin receives a hooks object and registers handlers for specific hook points. Plugins can modify prompt trees, inject content, process responses, and trigger additional LLM calls.
Each tool receives a hooks object and registers handlers for specific hook points. Tools can modify prompt trees, inject content, process responses, and trigger additional LLM calls.
### Example Plugin Structure
### Example Tool Structure
```typescript
export const myPlugin: PromptConcatPlugin = (hooks) => {
hooks.processPrompts.tapAsync('myPlugin', async (context, callback) => {
const { plugin, prompts, messages } = context;
// Plugin logic here
export const myTool: PromptConcatTool = (hooks) => {
hooks.processPrompts.tapAsync('myTool', async (context, callback) => {
const { tool, prompts, messages } = context;
// Tool logic here
callback(null, context);
});
};

View file

@ -10,17 +10,17 @@
* Main Concepts:
* - Prompts are tree-structured, can have roles (system/user/assistant) and children.
* - Plugins use hooks to modify the prompt tree at runtime.
* - Built-in plugins are registered by pluginId and executed when matching plugins are found.
* - Built-in tools are registered by toolId and executed when matching tools are found.
*/
import { logger } from '@services/libs/log';
import { ModelMessage } from 'ai';
import { cloneDeep } from 'lodash';
import { AgentHandlerContext } from '../buildInAgentHandlers/type';
import { AgentFrameworkContext } from '../agentFrameworks/utilities/type';
import { AgentInstanceMessage } from '../interface';
import { builtInPlugins, createHandlerHooks, PromptConcatHookContext } from '../plugins';
import { builtInTools, createAgentFrameworkHooks, PromptConcatHookContext } from '../tools';
import type { AgentPromptDescription, IPrompt } from './promptConcatSchema';
import type { IPromptConcatPlugin } from './promptConcatSchema/plugin';
import type { IPromptConcatTool } from './promptConcatSchema/plugin';
/**
* Context type specific for prompt concatenation operations
@ -37,7 +37,7 @@ export interface PromptConcatContext {
* Generate ID-based path mapping for prompts to enable source tracking
* Uses actual node IDs instead of indices to avoid path conflicts with dynamic content
*/
function generateSourcePaths(prompts: IPrompt[], plugins: IPromptConcatPlugin[] = []): Map<string, string[]> {
function generateSourcePaths(prompts: IPrompt[], plugins: IPromptConcatTool[] = []): Map<string, string[]> {
const pathMap = new Map<string, string[]>();
function traversePrompts(items: IPrompt[], currentPath: string[]): void {
items.forEach((item) => {
@ -48,7 +48,7 @@ function generateSourcePaths(prompts: IPrompt[], plugins: IPromptConcatPlugin[]
}
});
}
function traversePlugins(items: IPromptConcatPlugin[], currentPath: string[]): void {
function traversePlugins(items: IPromptConcatTool[], currentPath: string[]): void {
items.forEach((item) => {
const itemPath = [...currentPath, item.id];
pathMap.set(item.id, itemPath);
@ -191,7 +191,7 @@ export interface PromptConcatStreamState {
/** Current processing step */
step: 'plugin' | 'finalize' | 'flatten' | 'complete';
/** Current plugin being processed (if step is 'plugin') */
currentPlugin?: IPromptConcatPlugin;
currentPlugin?: IPromptConcatTool;
/** Processing progress (0-1) */
progress: number;
/** Whether processing is complete */
@ -203,40 +203,41 @@ export interface PromptConcatStreamState {
* Yields intermediate results for real-time UI updates
*/
export async function* promptConcatStream(
agentConfig: Pick<AgentPromptDescription, 'handlerConfig'>,
agentConfig: Pick<AgentPromptDescription, 'agentFrameworkConfig'>,
messages: AgentInstanceMessage[],
handlerContext: AgentHandlerContext,
agentFrameworkContext: AgentFrameworkContext,
): AsyncGenerator<PromptConcatStreamState, PromptConcatStreamState, unknown> {
const promptConfigs = Array.isArray(agentConfig.handlerConfig.prompts) ? agentConfig.handlerConfig.prompts : [];
const pluginConfigs = (Array.isArray(agentConfig.handlerConfig.plugins) ? agentConfig.handlerConfig.plugins : []) as IPromptConcatPlugin[];
const agentFrameworkConfig = agentConfig.agentFrameworkConfig;
const promptConfigs = Array.isArray(agentFrameworkConfig?.prompts) ? agentFrameworkConfig.prompts : [];
const toolConfigs = (Array.isArray(agentFrameworkConfig?.plugins) ? agentFrameworkConfig.plugins : []) as IPromptConcatTool[];
const promptsCopy = cloneDeep(promptConfigs);
const sourcePaths = generateSourcePaths(promptsCopy, pluginConfigs);
const sourcePaths = generateSourcePaths(promptsCopy, toolConfigs);
const hooks = createHandlerHooks();
// Register plugins that match the configuration
for (const plugin of pluginConfigs) {
const builtInPlugin = builtInPlugins.get(plugin.pluginId);
if (builtInPlugin) {
builtInPlugin(hooks);
logger.debug('Registered plugin', {
pluginId: plugin.pluginId,
pluginInstanceId: plugin.id,
const hooks = createAgentFrameworkHooks();
// Register tools that match the configuration
for (const tool of toolConfigs) {
const builtInTool = builtInTools.get(tool.toolId);
if (builtInTool) {
builtInTool(hooks);
logger.debug('Registered tool', {
toolId: tool.toolId,
toolInstanceId: tool.id,
});
} else {
logger.info(`No built-in plugin found for pluginId: ${plugin.pluginId}`);
logger.info(`No built-in tool found for toolId: ${tool.toolId}`);
}
}
// Process each plugin through hooks with streaming
let modifiedPrompts = promptsCopy;
const totalSteps = pluginConfigs.length + 2; // plugins + finalize + flatten
const totalSteps = toolConfigs.length + 2; // plugins + finalize + flatten
for (let index = 0; index < pluginConfigs.length; index++) {
for (let index = 0; index < toolConfigs.length; index++) {
const context: PromptConcatHookContext = {
handlerContext,
agentFrameworkContext: agentFrameworkContext,
messages,
prompts: modifiedPrompts,
pluginConfig: pluginConfigs[index],
toolConfig: toolConfigs[index],
metadata: { sourcePaths },
};
try {
@ -255,13 +256,13 @@ export async function* promptConcatStream(
processedPrompts: modifiedPrompts,
flatPrompts: intermediateFlat,
step: 'plugin',
currentPlugin: pluginConfigs[index],
currentPlugin: toolConfigs[index],
progress: (index + 1) / totalSteps,
isComplete: false,
};
} catch (error) {
logger.error('Plugin processing error', {
pluginConfig: pluginConfigs[index],
toolConfig: toolConfigs[index],
error,
});
// Continue processing other plugins even if one fails
@ -273,15 +274,15 @@ export async function* promptConcatStream(
processedPrompts: modifiedPrompts,
flatPrompts: flattenPrompts(modifiedPrompts),
step: 'finalize',
progress: (pluginConfigs.length + 1) / totalSteps,
progress: (toolConfigs.length + 1) / totalSteps,
isComplete: false,
};
const finalContext: PromptConcatHookContext = {
handlerContext,
agentFrameworkContext: agentFrameworkContext,
messages,
prompts: modifiedPrompts,
pluginConfig: {} as IPromptConcatPlugin, // Empty plugin for finalization
toolConfig: {} as IPromptConcatTool, // Empty tool for finalization
metadata: { sourcePaths },
};
@ -297,7 +298,7 @@ export async function* promptConcatStream(
processedPrompts: modifiedPrompts,
flatPrompts: flattenPrompts(modifiedPrompts),
step: 'flatten',
progress: (pluginConfigs.length + 2) / totalSteps,
progress: (toolConfigs.length + 2) / totalSteps,
isComplete: false,
};
@ -341,15 +342,15 @@ export async function* promptConcatStream(
* @returns Processed prompt array and original prompt tree
*/
export async function promptConcat(
agentConfig: Pick<AgentPromptDescription, 'handlerConfig'>,
agentConfig: Pick<AgentPromptDescription, 'agentFrameworkConfig'>,
messages: AgentInstanceMessage[],
handlerContext: AgentHandlerContext,
agentFrameworkContext: AgentFrameworkContext,
): Promise<{
flatPrompts: ModelMessage[];
processedPrompts: IPrompt[];
}> {
// Use the streaming version and just return the final result
const stream = promptConcatStream(agentConfig, messages, handlerContext);
const stream = promptConcatStream(agentConfig, messages, agentFrameworkContext);
let finalResult: PromptConcatStreamState;
// Consume all intermediate states to get the final result

View file

@ -1,4 +1,4 @@
import { createDynamicPromptConcatPluginSchema } from '@services/agentInstance/plugins/schemaRegistry';
import { createDynamicPromptConcatToolSchema } from '@services/agentInstance/tools/schemaRegistry';
import { t } from '@services/libs/i18n/placeholder';
import { z } from 'zod/v4';
import { ModelParametersSchema, ProviderModelSchema } from './modelParameters';
@ -34,12 +34,12 @@ export const AIConfigSchema = BaseAPIConfigSchema
});
/**
* Handler configuration schema
* Contains the handler-related configuration fields for prompts, responses, and plugins
* This is dynamically generated to include all registered plugins
* Framework configuration schema
* Contains the framework-related configuration fields for prompts, responses, and tools
* This is dynamically generated to include all registered tools
*/
export function getHandlerConfigSchema() {
const dynamicPluginSchema = createDynamicPromptConcatPluginSchema();
export function getFrameworkConfigSchema() {
const dynamicToolSchema = createDynamicPromptConcatToolSchema();
return z.object({
prompts: z.array(PromptSchema).meta({
@ -50,7 +50,7 @@ export function getHandlerConfigSchema() {
description: t('Schema.AgentConfig.PromptConfig.Response'),
title: t('PromptConfig.Tabs.Response'),
}),
plugins: z.array(dynamicPluginSchema).meta({
plugins: z.array(dynamicToolSchema).meta({
description: t('Schema.AgentConfig.PromptConfig.Plugins'),
title: t('PromptConfig.Tabs.Plugins'),
}),
@ -66,13 +66,13 @@ export function getHandlerConfigSchema() {
* @example
* ```json
* {
* "id": "example-agent",
* "id": "task-agent",
* "api": {
* "provider": "siliconflow",
* "model": "Qwen/Qwen2.5-7B-Instruct"
* },
* "modelParameters": { ... },
* "handlerConfig": {
* "agentFrameworkConfig": {
* "prompts": [ ... ],
* "response": [ ... ],
* "plugins": [ ... ],
@ -81,14 +81,14 @@ export function getHandlerConfigSchema() {
* ```
*/
export function getAgentConfigSchema() {
const dynamicHandlerConfigSchema = getHandlerConfigSchema();
const dynamicFrameworkConfigSchema = getFrameworkConfigSchema();
return BaseAPIConfigSchema.extend({
id: z.string().meta({
title: t('Schema.AgentConfig.IdTitle'),
description: t('Schema.AgentConfig.Id'),
}),
handlerConfig: dynamicHandlerConfigSchema,
agentFrameworkConfig: dynamicFrameworkConfigSchema,
}).meta({
title: t('Schema.AgentConfig.Title'),
description: t('Schema.AgentConfig.Description'),
@ -110,10 +110,15 @@ export function getDefaultAgentsSchema() {
export type DefaultAgents = z.infer<ReturnType<typeof getDefaultAgentsSchema>>;
export type AgentPromptDescription = z.infer<ReturnType<typeof getAgentConfigSchema>>;
export type AiAPIConfig = z.infer<typeof AIConfigSchema>;
export type HandlerConfig = z.infer<ReturnType<typeof getHandlerConfigSchema>>;
export type AgentFrameworkConfig = z.infer<ReturnType<typeof getFrameworkConfigSchema>>;
// Backward compat aliases - deprecated, use AgentFrameworkConfig directly
export type HandlerConfig = AgentFrameworkConfig;
// Re-export all schemas and types
export * from './modelParameters';
export * from './plugin';
export * from './prompts';
export * from './response';
// Export IPromptConcatTool as IPromptConcatPlugin for backward compatibility
export type { IPromptConcatTool as IPromptConcatPlugin } from './plugin';

View file

@ -1,5 +1,5 @@
import { z } from 'zod/v4';
import { getHandlerConfigSchema } from './index';
import { getFrameworkConfigSchema } from './index';
/**
* Get the dynamically generated JSON Schema for handler configuration
@ -11,7 +11,7 @@ import { getHandlerConfigSchema } from './index';
*
* Description field is i18n key, use i18nAlly extension to see it on VSCode. And use react-i18next to translate it on frontend.
*/
export function getPromptConcatHandlerConfigJsonSchema() {
const dynamicHandlerConfigSchema = getHandlerConfigSchema();
return z.toJSONSchema(dynamicHandlerConfigSchema, { target: 'draft-7' });
export function getPromptConcatAgentFrameworkConfigJsonSchema() {
const dynamicFrameworkConfigSchema = getFrameworkConfigSchema();
return z.toJSONSchema(dynamicFrameworkConfigSchema, { target: 'draft-7' });
}

View file

@ -1,22 +1,22 @@
// Import parameter types from plugin files
import type { ModelContextProtocolParameter } from '@services/agentInstance/plugins/modelContextProtocolPlugin';
import type { DynamicPositionParameter, FullReplacementParameter } from '@services/agentInstance/plugins/promptPlugins';
import type { WikiOperationParameter } from '@services/agentInstance/plugins/wikiOperationPlugin';
import type { WikiSearchParameter } from '@services/agentInstance/plugins/wikiSearchPlugin';
import type { WorkspacesListParameter } from '@services/agentInstance/plugins/workspacesListPlugin';
import type { ModelContextProtocolParameter } from '@services/agentInstance/tools/modelContextProtocol';
import type { DynamicPositionParameter, FullReplacementParameter } from '@services/agentInstance/tools/prompt';
import type { WikiOperationParameter } from '@services/agentInstance/tools/wikiOperation';
import type { WikiSearchParameter } from '@services/agentInstance/tools/wikiSearch';
import type { WorkspacesListParameter } from '@services/agentInstance/tools/workspacesList';
/**
* Type definition for prompt concat plugin
* Type definition for prompt concat tool
* This includes all possible parameter fields for type safety
*/
export type IPromptConcatPlugin = {
export type IPromptConcatTool = {
id: string;
caption?: string;
content?: string;
forbidOverrides?: boolean;
pluginId: string;
toolId: string;
// Plugin-specific parameters
// Tool-specific parameters
fullReplacementParam?: FullReplacementParameter;
dynamicPositionParam?: DynamicPositionParameter;
wikiOperationParam?: WikiOperationParameter;

View file

@ -6,12 +6,12 @@
import { ToolCallingMatch } from '@services/agentDefinition/interface';
import { logger } from '@services/libs/log';
import { cloneDeep } from 'lodash';
import { AgentHandlerContext } from '../buildInAgentHandlers/type';
import { AgentFrameworkContext } from '../agentFrameworks/utilities/type';
import { AgentInstanceMessage } from '../interface';
import { builtInPlugins, createHandlerHooks } from '../plugins';
import { AgentResponse, PostProcessContext, YieldNextRoundTarget } from '../plugins/types';
import type { IPromptConcatPlugin } from './promptConcatSchema';
import { AgentPromptDescription, HandlerConfig } from './promptConcatSchema';
import { builtInTools, createAgentFrameworkHooks } from '../tools';
import { AgentResponse, PostProcessContext, YieldNextRoundTarget } from '../tools/types';
import type { IPromptConcatTool } from './promptConcatSchema';
import { AgentFrameworkConfig, AgentPromptDescription } from './promptConcatSchema';
/**
* Process response configuration, apply plugins, and return final response
@ -24,7 +24,7 @@ import { AgentPromptDescription, HandlerConfig } from './promptConcatSchema';
export async function responseConcat(
agentConfig: AgentPromptDescription,
llmResponse: string,
context: AgentHandlerContext,
context: AgentFrameworkContext,
messages: AgentInstanceMessage[] = [],
): Promise<{
processedResponse: string;
@ -38,33 +38,33 @@ export async function responseConcat(
responseLength: llmResponse.length,
});
const { handlerConfig } = agentConfig;
const responses: HandlerConfig['response'] = Array.isArray(handlerConfig.response) ? handlerConfig.response : [];
const plugins = (Array.isArray(handlerConfig.plugins) ? handlerConfig.plugins : []) as IPromptConcatPlugin[];
const { agentFrameworkConfig } = agentConfig;
const responses: AgentFrameworkConfig['response'] = Array.isArray(agentFrameworkConfig?.response) ? (agentFrameworkConfig?.response || []) : [];
const toolConfigs = (Array.isArray(agentFrameworkConfig.plugins) ? agentFrameworkConfig.plugins : []) as IPromptConcatTool[];
let modifiedResponses = cloneDeep(responses) as AgentResponse[];
// Create hooks instance
const hooks = createHandlerHooks();
// Register all plugins from configuration
for (const plugin of plugins) {
const builtInPlugin = builtInPlugins.get(plugin.pluginId);
if (builtInPlugin) {
builtInPlugin(hooks);
const hooks = createAgentFrameworkHooks();
// Register all tools from configuration
for (const tool of toolConfigs) {
const builtInTool = builtInTools.get(tool.toolId);
if (builtInTool) {
builtInTool(hooks);
} else {
logger.warn(`No built-in plugin found for response pluginId: ${plugin.pluginId}`);
logger.warn(`No built-in tool found for response toolId: ${tool.toolId}`);
}
}
// Process each plugin through hooks
// Process each tool through hooks
let yieldNextRoundTo: YieldNextRoundTarget | undefined;
let toolCallInfo: ToolCallingMatch | undefined;
for (const plugin of plugins) {
for (const tool of toolConfigs) {
const responseContext: PostProcessContext = {
handlerContext: context,
agentFrameworkContext: context,
messages,
prompts: [], // Not used in response processing
pluginConfig: plugin,
toolConfig: tool,
llmResponse,
responses: modifiedResponses,
metadata: {},
@ -78,31 +78,31 @@ export async function responseConcat(
modifiedResponses = result.responses;
}
// Check if plugin indicated need for new LLM call via actions
// Check if tool indicated need for new LLM call via actions
if (result.actions?.yieldNextRoundTo) {
yieldNextRoundTo = result.actions.yieldNextRoundTo;
if (result.actions.toolCalling) {
toolCallInfo = result.actions.toolCalling;
}
logger.debug('Plugin requested yield next round', {
pluginId: plugin.pluginId,
pluginInstanceId: plugin.id,
logger.debug('Tool requested yield next round', {
toolId: tool.toolId,
toolInstanceId: tool.id,
yieldNextRoundTo,
hasToolCall: !!result.actions.toolCalling,
});
}
logger.debug('Response plugin processed successfully', {
pluginId: plugin.pluginId,
pluginInstanceId: plugin.id,
logger.debug('Response tool processed successfully', {
toolId: tool.toolId,
toolInstanceId: tool.id,
});
} catch (error) {
logger.error('Response plugin processing error', {
pluginId: plugin.pluginId,
pluginInstanceId: plugin.id,
logger.error('Response tool processing error', {
toolId: tool.toolId,
toolInstanceId: tool.id,
error,
});
// Continue processing other plugins even if one fails
// Continue processing other tools even if one fails
}
}

View file

@ -1,21 +1,21 @@
/**
* Tests for Full Replacement plugin duration mechanism
* Tests that expired messages (with duration) are filtered out from AI context
* Based on real configuration from defaultAgents.json
* Based on real configuration from taskAgents.json
*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { AgentInstanceMessage } from '../../interface';
import type { IPromptConcatPlugin } from '../../promptConcat/promptConcatSchema';
import type { IPromptConcatTool } from '../../promptConcat/promptConcatSchema';
import type { IPrompt } from '../../promptConcat/promptConcatSchema/prompts';
import { cloneDeep } from 'lodash';
import defaultAgents from '../../buildInAgentHandlers/defaultAgents.json';
import { createHandlerHooks, PromptConcatHookContext } from '../index';
import { fullReplacementPlugin } from '../promptPlugins';
import defaultAgents from '../../agentFrameworks/taskAgents.json';
import { createAgentFrameworkHooks, PromptConcatHookContext } from '../index';
import { fullReplacementTool } from '../prompt';
// Use the real agent config
const exampleAgent = defaultAgents[0];
const realHandlerConfig = exampleAgent.handlerConfig;
const realAgentFrameworkConfig = exampleAgent.agentFrameworkConfig;
describe('Full Replacement Plugin - Duration Mechanism', () => {
beforeEach(() => {
@ -24,15 +24,15 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
describe('History Source Type with Duration Filtering', () => {
it('should filter out expired messages (duration=1) from historyOfSession', async () => {
// Find the real fullReplacement plugin for history from defaultAgents.json
const historyPlugin = realHandlerConfig.plugins.find(
p => p.pluginId === 'fullReplacement' && p.fullReplacementParam?.sourceType === 'historyOfSession',
// Find the real fullReplacement plugin for history from taskAgents.json
const historyPlugin = realAgentFrameworkConfig.plugins.find(
p => p.toolId === 'fullReplacement' && p.fullReplacementParam?.sourceType === 'historyOfSession',
);
expect(historyPlugin).toBeDefined();
expect(historyPlugin!.fullReplacementParam!.targetId).toBe('default-history'); // Real target ID
// Use real prompts structure from defaultAgents.json
const testPrompts = cloneDeep(realHandlerConfig.prompts) as IPrompt[];
// Use real prompts structure from taskAgents.json
const testPrompts = cloneDeep(realAgentFrameworkConfig.prompts) as IPrompt[];
const messages: AgentInstanceMessage[] = [
// Message 0: User message, no duration - should be included
@ -96,7 +96,7 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
];
const context: PromptConcatHookContext = {
handlerContext: {
agentFrameworkContext: {
agent: {
id: 'test-agent',
messages,
@ -104,16 +104,16 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
status: { state: 'working' as const, modified: new Date() },
created: new Date(),
},
agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} },
agentDef: { id: 'test-agent-def', name: 'test', agentFrameworkConfig: {} },
isCancelled: () => false,
},
pluginConfig: historyPlugin! as unknown as IPromptConcatPlugin, // Type cast due to JSON import limitations
toolConfig: historyPlugin! as unknown as IPromptConcatTool, // Type cast due to JSON import limitations
prompts: testPrompts,
messages,
};
const hooks = createHandlerHooks();
fullReplacementPlugin(hooks);
const hooks = createAgentFrameworkHooks();
fullReplacementTool(hooks);
// Execute the processPrompts hook
await hooks.processPrompts.promise(context);
@ -126,8 +126,8 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
const targetPrompt = historyPrompt!.children?.find(child => child.id === targetId);
expect(targetPrompt).toBeDefined();
// The fullReplacementPlugin puts filtered messages in children array
// Note: fullReplacementPlugin removes the last message (current user message)
// The fullReplacementTool puts filtered messages in children array
// Note: fullReplacementTool removes the last message (current user message)
const children = (targetPrompt as unknown as { children?: IPrompt[] }).children || [];
expect(children.length).toBe(2); // Only non-expired messages (user1, ai-response), excluding last user message
@ -147,8 +147,8 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
});
it('should include messages with duration=0 (visible in current round)', async () => {
const historyPlugin = realHandlerConfig.plugins.find(
p => p.pluginId === 'fullReplacement' && p.fullReplacementParam?.sourceType === 'historyOfSession',
const historyPlugin = realAgentFrameworkConfig.plugins.find(
p => p.toolId === 'fullReplacement' && p.fullReplacementParam?.sourceType === 'historyOfSession',
);
const messages: AgentInstanceMessage[] = [
@ -181,10 +181,10 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
},
];
const testPrompts = cloneDeep(realHandlerConfig.prompts) as IPrompt[];
const testPrompts = cloneDeep(realAgentFrameworkConfig.prompts) as IPrompt[];
const context: PromptConcatHookContext = {
handlerContext: {
agentFrameworkContext: {
agent: {
id: 'test-agent',
messages,
@ -192,16 +192,16 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
status: { state: 'working' as const, modified: new Date() },
created: new Date(),
},
agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} },
agentDef: { id: 'test-agent-def', name: 'test', agentFrameworkConfig: {} },
isCancelled: () => false,
},
pluginConfig: historyPlugin! as unknown as IPromptConcatPlugin, // Type cast for JSON import
toolConfig: historyPlugin! as unknown as IPromptConcatTool, // Type cast for JSON import
prompts: testPrompts,
messages,
};
const hooks = createHandlerHooks();
fullReplacementPlugin(hooks);
const hooks = createAgentFrameworkHooks();
fullReplacementTool(hooks);
await hooks.processPrompts.promise(context);
@ -220,8 +220,8 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
});
it('should handle mixed duration values correctly', async () => {
const historyPlugin = realHandlerConfig.plugins.find(
p => p.pluginId === 'fullReplacement' && p.fullReplacementParam?.sourceType === 'historyOfSession',
const historyPlugin = realAgentFrameworkConfig.plugins.find(
p => p.toolId === 'fullReplacement' && p.fullReplacementParam?.sourceType === 'historyOfSession',
);
const messages: AgentInstanceMessage[] = [
@ -263,10 +263,10 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
},
];
const testPrompts = cloneDeep(realHandlerConfig.prompts) as IPrompt[];
const testPrompts = cloneDeep(realAgentFrameworkConfig.prompts) as IPrompt[];
const context: PromptConcatHookContext = {
handlerContext: {
agentFrameworkContext: {
agent: {
id: 'test-agent',
messages,
@ -274,16 +274,16 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
status: { state: 'working' as const, modified: new Date() },
created: new Date(),
},
agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} },
agentDef: { id: 'test-agent-def', name: 'test', agentFrameworkConfig: {} },
isCancelled: () => false,
},
pluginConfig: historyPlugin! as unknown as IPromptConcatPlugin, // Type cast for JSON import
toolConfig: historyPlugin! as unknown as IPromptConcatTool, // Type cast for JSON import
prompts: testPrompts,
messages,
};
const hooks = createHandlerHooks();
fullReplacementPlugin(hooks);
const hooks = createAgentFrameworkHooks();
fullReplacementTool(hooks);
await hooks.processPrompts.promise(context);
@ -308,8 +308,8 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
describe('LLM Response Source Type', () => {
it('should verify LLM response replacement config exists', () => {
// Verify the real config has LLM response replacement
const llmResponsePlugin = realHandlerConfig.plugins.find(
p => p.pluginId === 'fullReplacement' && p.fullReplacementParam?.sourceType === 'llmResponse',
const llmResponsePlugin = realAgentFrameworkConfig.plugins.find(
p => p.toolId === 'fullReplacement' && p.fullReplacementParam?.sourceType === 'llmResponse',
);
expect(llmResponsePlugin).toBeDefined();
expect(llmResponsePlugin!.fullReplacementParam!.targetId).toBe('default-response');

View file

@ -1,6 +1,6 @@
/**
* Deep integration tests for messageManagementPlugin with real SQLite database
* Tests actual message persistence scenarios using defaultAgents.json configuration
* Deep integration tests for messageManagementTool with real SQLite database
* Tests actual message persistence scenarios using taskAgents.json configuration
*/
import { container } from '@services/container';
import type { IDatabaseService } from '@services/database/interface';
@ -8,20 +8,20 @@ import { AgentDefinitionEntity, AgentInstanceEntity, AgentInstanceMessageEntity
import serviceIdentifier from '@services/serviceIdentifier';
import { DataSource } from 'typeorm';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import defaultAgents from '../../buildInAgentHandlers/defaultAgents.json';
import defaultAgents from '../../agentFrameworks/taskAgents.json';
import type { AgentInstanceMessage, IAgentInstanceService } from '../../interface';
import { createHandlerHooks } from '../index';
import { messageManagementPlugin } from '../messageManagementPlugin';
import { createAgentFrameworkHooks } from '../index';
import { messageManagementTool } from '../messageManagement';
import type { ToolExecutionContext, UserMessageContext } from '../types';
// Use the real agent config from defaultAgents.json
// Use the real agent config from taskAgents.json
const exampleAgent = defaultAgents[0];
describe('Message Management Plugin - Real Database Integration', () => {
let testAgentId: string;
// agentInstanceServiceImpl available to test blocks
let agentInstanceServiceImpl: IAgentInstanceService;
let hooks: ReturnType<typeof createHandlerHooks>;
let hooks: ReturnType<typeof createAgentFrameworkHooks>;
let realDataSource: DataSource;
beforeEach(async () => {
@ -69,15 +69,15 @@ describe('Message Management Plugin - Real Database Integration', () => {
await agentInstanceServiceImpl.initialize();
// Initialize plugin
hooks = createHandlerHooks();
messageManagementPlugin(hooks);
hooks = createAgentFrameworkHooks();
messageManagementTool(hooks);
});
afterEach(async () => {
// Clean up is handled automatically by beforeEach for each test
});
const createHandlerContext = (messages: AgentInstanceMessage[] = []) => ({
const createAgentFrameworkContext = (messages: AgentInstanceMessage[] = []) => ({
agent: {
id: testAgentId,
agentDefId: exampleAgent.id,
@ -90,19 +90,19 @@ describe('Message Management Plugin - Real Database Integration', () => {
name: exampleAgent.name,
version: '1.0.0',
capabilities: [],
handlerConfig: exampleAgent.handlerConfig,
agentFrameworkConfig: exampleAgent.agentFrameworkConfig,
},
isCancelled: () => false,
});
describe('Real Wiki Search Scenario - The Missing Tool Result Bug', () => {
it('should persist all messages in wiki search flow: user query → AI tool call → tool result → AI final response', async () => {
const handlerContext = createHandlerContext();
const agentFrameworkContext = createAgentFrameworkContext();
// Step 1: User asks to search wiki
const userMessageId = `user-msg-${Date.now()}`;
const userContext: UserMessageContext = {
handlerContext,
agentFrameworkContext,
content: { text: '搜索 wiki 中的 Index 条目并解释' },
messageId: userMessageId,
timestamp: new Date(),
@ -133,10 +133,10 @@ describe('Message Management Plugin - Real Database Integration', () => {
};
await agentInstanceServiceImpl.saveUserMessage(aiToolCallMessage);
handlerContext.agent.messages.push(aiToolCallMessage);
agentFrameworkContext.agent.messages.push(aiToolCallMessage);
// Step 3: Tool result message (THIS IS THE MISSING PIECE!)
// This simulates what wikiSearchPlugin does when tool execution completes
// This simulates what wikiSearchTool does when tool execution completes
const toolResultMessage: AgentInstanceMessage = {
id: `tool-result-${Date.now()}`,
agentId: testAgentId,
@ -164,11 +164,11 @@ Result: 在wiki中找到了名为"Index"的条目。这个条目包含以下内
duration: 10, // Tool results might have expiration
};
// Add tool result to agent messages (simulating what wikiSearchPlugin does)
handlerContext.agent.messages.push(toolResultMessage);
// Add tool result to agent messages (simulating what wikiSearchTool does)
agentFrameworkContext.agent.messages.push(toolResultMessage);
const toolContext: ToolExecutionContext = {
handlerContext,
agentFrameworkContext,
toolResult: {
success: true,
data: 'Wiki search completed successfully',
@ -202,7 +202,7 @@ Result: 在wiki中找到了名为"Index"的条目。这个条目包含以下内
expect(savedToolResult?.duration).toBe(10);
// Verify isPersisted flag was updated
const toolMessageInMemory = handlerContext.agent.messages.find(
const toolMessageInMemory = agentFrameworkContext.agent.messages.find(
(m) => m.metadata?.isToolResult,
);
expect(toolMessageInMemory?.metadata?.isPersisted).toBe(true);
@ -249,7 +249,7 @@ Result: 在wiki中找到了名为"Index"的条目。这个条目包含以下内
});
it('should handle multiple tool results in one execution', async () => {
const handlerContext = createHandlerContext();
const agentFrameworkContext = createAgentFrameworkContext();
// Add multiple tool result messages
const toolResult1: AgentInstanceMessage = {
@ -282,10 +282,10 @@ Result: 在wiki中找到了名为"Index"的条目。这个条目包含以下内
duration: 3,
};
handlerContext.agent.messages.push(toolResult1, toolResult2);
agentFrameworkContext.agent.messages.push(toolResult1, toolResult2);
const toolContext: ToolExecutionContext = {
handlerContext,
agentFrameworkContext,
toolResult: {
success: true,
data: 'Multiple tool search completed',
@ -316,7 +316,7 @@ Result: 在wiki中找到了名为"Index"的条目。这个条目包含以下内
it('should maintain message integrity when reloading from database (simulating page refresh)', async () => {
// This test simulates the issue where tool results are missing after page refresh
const handlerContext = createHandlerContext();
const agentFrameworkContext = createAgentFrameworkContext();
// Step 1: Complete chat flow with user message → AI tool call → tool result → AI response
const userMessage: AgentInstanceMessage = {
@ -372,9 +372,9 @@ Result: 在wiki中找到了名为"Index"的条目。这个条目包含以下内
await agentInstanceServiceImpl.saveUserMessage(aiToolCallMessage);
// Add tool result to context and trigger persistence via toolExecuted hook
handlerContext.agent.messages.push(toolResultMessage);
agentFrameworkContext.agent.messages.push(toolResultMessage);
const toolContext: ToolExecutionContext = {
handlerContext,
agentFrameworkContext,
toolResult: { success: true, data: 'Search completed' },
toolInfo: { toolId: 'wiki-search', parameters: {} },
};

View file

@ -1,5 +1,5 @@
/**
* Tests for wikiOperationPlugin
* Tests for wikiOperationTool
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@ -11,16 +11,16 @@ import type { IWikiService } from '@services/wiki/interface';
// Removed logger import as it is unused
import { matchToolCalling } from '@services/agentDefinition/responsePatternUtility';
import type { IPromptConcatPlugin } from '@services/agentInstance/promptConcat/promptConcatSchema';
import type { IPromptConcatTool } from '@services/agentInstance/promptConcat/promptConcatSchema';
import type { IPrompt } from '@services/agentInstance/promptConcat/promptConcatSchema';
import type { AIStreamResponse } from '@services/externalAPI/interface';
import type { IWorkspaceService } from '@services/workspaces/interface';
import type { AgentHandlerContext } from '../../buildInAgentHandlers/type';
import type { AgentFrameworkContext } from '../../agentFrameworks/utilities/type';
import type { AgentInstance } from '../../interface';
import { createHandlerHooks } from '../index';
import type { AIResponseContext, PluginActions, PromptConcatHookContext } from '../types';
import { wikiOperationPlugin } from '../wikiOperationPlugin';
import { workspacesListPlugin } from '../workspacesListPlugin';
import { createAgentFrameworkHooks } from '../index';
import type { AIResponseContext, PromptConcatHookContext, ToolActions } from '../types';
import { wikiOperationTool } from '../wikiOperation';
import { workspacesListTool } from '../workspacesList';
// Mock i18n
vi.mock('@services/libs/i18n', () => ({
@ -50,8 +50,8 @@ vi.mock('@services/libs/i18n', () => ({
},
}));
// Helper to construct a complete AgentHandlerContext for tests
const makeHandlerContext = (agentId = 'test-agent'): AgentHandlerContext => ({
// Helper to construct a complete AgentagentFrameworkContext for tests
const makeAgentFrameworkContext = (agentId = 'test-agent'): AgentFrameworkContext => ({
agent: {
id: agentId,
agentDefId: 'test-agent-def',
@ -59,11 +59,11 @@ const makeHandlerContext = (agentId = 'test-agent'): AgentHandlerContext => ({
status: { state: 'working', modified: new Date() },
created: new Date(),
} as unknown as AgentInstance,
agentDef: { id: 'test-agent-def', name: 'test-agent-def', handlerConfig: {} } as unknown as { id: string; name: string; handlerConfig: Record<string, unknown> },
agentDef: { id: 'test-agent-def', name: 'test-agent-def', agentFrameworkConfig: {} } as unknown as { id: string; name: string; agentFrameworkConfig: Record<string, unknown> },
isCancelled: () => false,
});
describe('wikiOperationPlugin', () => {
describe('wikiOperationTool', () => {
beforeEach(async () => {
vi.clearAllMocks();
});
@ -73,12 +73,12 @@ describe('wikiOperationPlugin', () => {
});
it('should inject wiki operation tool content when plugin is configured', async () => {
const hooks = createHandlerHooks();
// First register workspacesListPlugin to inject available workspaces from the global mock
workspacesListPlugin(hooks);
wikiOperationPlugin(hooks);
const hooks = createAgentFrameworkHooks();
// First register workspacesListTool to inject available workspaces from the global mock
workspacesListTool(hooks);
wikiOperationTool(hooks);
// Start with prompts and run workspacesList injection first (pluginConfig for workspacesList)
// Start with prompts and run workspacesList injection first (toolConfig for workspacesList)
const prompts: IPrompt[] = [
{
id: 'target-prompt',
@ -88,48 +88,48 @@ describe('wikiOperationPlugin', () => {
];
const workspacesContext: PromptConcatHookContext = {
handlerContext: {
agentFrameworkContext: {
agent: { id: 'test-agent', messages: [], agentDefId: 'test', status: { state: 'working' as const, modified: new Date() }, created: new Date() },
agentDef: { id: 'test', name: 'test', handlerConfig: {} },
agentDef: { id: 'test', name: 'test', agentFrameworkConfig: {} },
isCancelled: () => false,
},
messages: [],
prompts,
pluginConfig: {
toolConfig: {
id: 'workspaces-plugin',
caption: 'Workspaces Plugin',
forbidOverrides: false,
pluginId: 'workspacesList',
toolId: 'workspacesList',
workspacesListParam: {
targetId: 'target-prompt',
position: 'after' as const,
},
} as unknown as IPromptConcatPlugin,
} as unknown as IPromptConcatTool,
};
await hooks.processPrompts.promise(workspacesContext);
// Then run wikiOperation injection which will append its tool content to the same prompt
const wikiOpContext: PromptConcatHookContext = {
handlerContext: workspacesContext.handlerContext,
agentFrameworkContext: workspacesContext.agentFrameworkContext,
messages: [],
prompts,
pluginConfig: {
toolConfig: {
id: 'test-plugin',
pluginId: 'wikiOperation',
toolId: 'wikiOperation',
wikiOperationParam: {
toolListPosition: {
targetId: 'target-prompt',
position: 'after' as const,
},
},
} as unknown as IPromptConcatPlugin,
} as unknown as IPromptConcatTool,
};
await hooks.processPrompts.promise(wikiOpContext);
const targetPrompt = prompts[0];
// workspacesListPlugin and wikiOperationPlugin may both add children; assert the combined children text contains expected snippets
// workspacesListTool and wikiOperationTool may both add children; assert the combined children text contains expected snippets
const childrenText = JSON.stringify(targetPrompt.children);
expect(childrenText).toContain('wiki-operation');
// Ensure the injected tool content documents the supported operations (enum values)
@ -147,17 +147,17 @@ describe('wikiOperationPlugin', () => {
describe('tool execution', () => {
it('should execute create operation successfully', async () => {
const hooks = createHandlerHooks();
wikiOperationPlugin(hooks);
const hooks = createAgentFrameworkHooks();
wikiOperationTool(hooks);
const handlerContext = makeHandlerContext();
const agentFrameworkContext = makeAgentFrameworkContext();
const context = {
handlerContext,
handlerConfig: {
agentFrameworkContext,
agentFrameworkConfig: {
plugins: [
{
pluginId: 'wikiOperation',
toolId: 'wikiOperation',
wikiOperationParam: {
toolResultDuration: 1,
},
@ -183,9 +183,9 @@ describe('wikiOperationPlugin', () => {
context.response.content = `<tool_use name="wiki-operation">${JSON.stringify(createParams)}</tool_use>`;
// Add an assistant message containing the tool_use so the plugin can find it
handlerContext.agent.messages.push({
agentFrameworkContext.agent.messages.push({
id: `m-${Date.now()}`,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
role: 'assistant',
content: context.response.content,
modified: new Date(),
@ -209,13 +209,13 @@ describe('wikiOperationPlugin', () => {
expect(typeof wikiSvc.wikiOperationInServer).toBe('function');
const responseCtx: AIResponseContext = {
handlerContext,
pluginConfig: context.handlerConfig?.plugins?.[0] as unknown as IPromptConcatPlugin,
handlerConfig: context.handlerConfig as { plugins?: Array<{ pluginId: string; [key: string]: unknown }> },
agentFrameworkContext,
toolConfig: context.agentFrameworkConfig?.plugins?.[0] as unknown as IPromptConcatTool,
agentFrameworkConfig: context.agentFrameworkConfig as { plugins?: Array<{ toolId: string; [key: string]: unknown }> },
response: { requestId: 'r-create', content: context.response.content, status: 'done' } as AIStreamResponse,
requestId: 'r-create',
isFinal: true,
actions: {} as PluginActions,
actions: {} as ToolActions,
};
await hooks.responseComplete.promise(responseCtx);
@ -227,7 +227,7 @@ describe('wikiOperationPlugin', () => {
);
// Verify a tool result message was added to agent history
const toolResultMessage = handlerContext.agent.messages.find(m => m.metadata?.isToolResult);
const toolResultMessage = agentFrameworkContext.agent.messages.find(m => m.metadata?.isToolResult);
expect(toolResultMessage).toBeTruthy();
expect(toolResultMessage?.content).toContain('<functions_result>');
// Check for general success wording and tiddler title
@ -238,15 +238,15 @@ describe('wikiOperationPlugin', () => {
});
it('should execute update operation successfully', async () => {
const hooks = createHandlerHooks();
wikiOperationPlugin(hooks);
const hooks = createAgentFrameworkHooks();
wikiOperationTool(hooks);
const handlerContext = makeHandlerContext();
const agentFrameworkContext = makeAgentFrameworkContext();
const context = {
handlerContext,
handlerConfig: {
plugins: [{ pluginId: 'wikiOperation', wikiOperationParam: {} }],
agentFrameworkContext,
agentFrameworkConfig: {
plugins: [{ toolId: 'wikiOperation', wikiOperationParam: {} }],
},
response: {
status: 'done' as const,
@ -256,9 +256,9 @@ describe('wikiOperationPlugin', () => {
};
// Add assistant message so plugin can detect the tool call
handlerContext.agent.messages.push({
agentFrameworkContext.agent.messages.push({
id: `m-${Date.now()}`,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
role: 'assistant',
content: context.response.content,
modified: new Date(),
@ -274,11 +274,11 @@ describe('wikiOperationPlugin', () => {
context.response.content = `<tool_use name="wiki-operation">${JSON.stringify(updateParams)}</tool_use>`;
const respCtx2: AIResponseContext = {
handlerContext,
pluginConfig: context.handlerConfig?.plugins?.[0] as unknown as IPromptConcatPlugin,
handlerConfig: context.handlerConfig as { plugins?: Array<{ pluginId: string; [key: string]: unknown }> },
agentFrameworkContext,
toolConfig: context.agentFrameworkConfig?.plugins?.[0] as unknown as IPromptConcatTool,
agentFrameworkConfig: context.agentFrameworkConfig as { plugins?: Array<{ toolId: string; [key: string]: unknown }> },
response: { requestId: 'r-update', content: context.response.content, status: 'done' } as AIStreamResponse,
actions: {} as PluginActions,
actions: {} as ToolActions,
requestId: 'r-update',
isFinal: true,
};
@ -291,22 +291,22 @@ describe('wikiOperationPlugin', () => {
);
// Check general update success wording and tiddler title
const updateResult = handlerContext.agent.messages.find(m => m.metadata?.isToolResult);
const updateResult = agentFrameworkContext.agent.messages.find(m => m.metadata?.isToolResult);
expect(updateResult).toBeTruthy();
expect(updateResult?.content).toContain('成功在Wiki工作空间');
expect(updateResult?.content).toContain('Existing Note');
});
it('should execute delete operation successfully', async () => {
const hooks = createHandlerHooks();
wikiOperationPlugin(hooks);
const hooks = createAgentFrameworkHooks();
wikiOperationTool(hooks);
const handlerContext = makeHandlerContext();
const agentFrameworkContext = makeAgentFrameworkContext();
const context = {
handlerContext,
handlerConfig: {
plugins: [{ pluginId: 'wikiOperation', wikiOperationParam: {} }],
agentFrameworkContext,
agentFrameworkConfig: {
plugins: [{ toolId: 'wikiOperation', wikiOperationParam: {} }],
},
response: {
status: 'done' as const,
@ -316,9 +316,9 @@ describe('wikiOperationPlugin', () => {
};
// Add assistant message so plugin can detect the tool call
handlerContext.agent.messages.push({
agentFrameworkContext.agent.messages.push({
id: `m-${Date.now()}`,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
role: 'assistant',
content: context.response.content,
modified: new Date(),
@ -333,11 +333,11 @@ describe('wikiOperationPlugin', () => {
context.response.content = `<tool_use name="wiki-operation">${JSON.stringify(deleteParams)}</tool_use>`;
const respCtx3: AIResponseContext = {
handlerContext,
pluginConfig: context.handlerConfig?.plugins?.[0] as unknown as IPromptConcatPlugin,
handlerConfig: context.handlerConfig as { plugins?: Array<{ pluginId: string; [key: string]: unknown }> },
agentFrameworkContext,
toolConfig: context.agentFrameworkConfig?.plugins?.[0] as unknown as IPromptConcatTool,
agentFrameworkConfig: context.agentFrameworkConfig as { plugins?: Array<{ toolId: string; [key: string]: unknown }> },
response: { requestId: 'r-delete', content: context.response.content, status: 'done' } as AIStreamResponse,
actions: {} as PluginActions,
actions: {} as ToolActions,
requestId: 'r-delete',
isFinal: true,
};
@ -349,22 +349,22 @@ describe('wikiOperationPlugin', () => {
['Note to Delete'],
);
const deleteResult = handlerContext.agent.messages.find(m => m.metadata?.isToolResult);
const deleteResult = agentFrameworkContext.agent.messages.find(m => m.metadata?.isToolResult);
expect(deleteResult).toBeTruthy();
expect(deleteResult?.content).toContain('成功从Wiki工作空间');
});
it('should handle workspace not found error', async () => {
const hooks = createHandlerHooks();
wikiOperationPlugin(hooks);
const hooks = createAgentFrameworkHooks();
wikiOperationTool(hooks);
// Use an actual tool_use payload with a nonexistent workspace
const handlerContext = makeHandlerContext();
const agentFrameworkContext = makeAgentFrameworkContext();
const context = {
handlerContext,
handlerConfig: {
plugins: [{ pluginId: 'wikiOperation', wikiOperationParam: {} }],
agentFrameworkContext,
agentFrameworkConfig: {
plugins: [{ toolId: 'wikiOperation', wikiOperationParam: {} }],
},
response: {
status: 'done',
@ -374,9 +374,9 @@ describe('wikiOperationPlugin', () => {
};
// Add assistant message so plugin can detect the tool call
handlerContext.agent.messages.push({
agentFrameworkContext.agent.messages.push({
id: `m-${Date.now()}`,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
role: 'assistant',
content: context.response.content,
modified: new Date(),
@ -390,17 +390,17 @@ describe('wikiOperationPlugin', () => {
context.response.content = `<tool_use name="wiki-operation">${JSON.stringify(badParams)}</tool_use>`;
const respCtx4: AIResponseContext = {
handlerContext,
pluginConfig: context.handlerConfig?.plugins?.[0] as unknown as IPromptConcatPlugin,
handlerConfig: context.handlerConfig as { plugins?: Array<{ pluginId: string; [key: string]: unknown }> },
agentFrameworkContext,
toolConfig: context.agentFrameworkConfig?.plugins?.[0] as unknown as IPromptConcatTool,
agentFrameworkConfig: context.agentFrameworkConfig as { plugins?: Array<{ toolId: string; [key: string]: unknown }> },
response: { requestId: 'r-error', content: context.response.content, status: 'done' } as AIStreamResponse,
actions: {} as PluginActions,
actions: {} as ToolActions,
requestId: 'r-error',
isFinal: true,
};
await hooks.responseComplete.promise(respCtx4);
const errResult = handlerContext.agent.messages.find(m => m.metadata?.isToolResult);
const errResult = agentFrameworkContext.agent.messages.find(m => m.metadata?.isToolResult);
expect(errResult).toBeTruthy();
expect(errResult?.content).toContain('工作空间名称或ID');
// Ensure control is yielded to self on error so AI gets the next round
@ -408,17 +408,17 @@ describe('wikiOperationPlugin', () => {
});
it('should not execute when tool call is not found', async () => {
const hooks = createHandlerHooks();
wikiOperationPlugin(hooks);
const hooks = createAgentFrameworkHooks();
wikiOperationTool(hooks);
// No tool_use in response
const handlerContext = makeHandlerContext();
const agentFrameworkContext = makeAgentFrameworkContext();
const context = {
handlerContext,
handlerConfig: {
plugins: [{ pluginId: 'wikiOperation', wikiOperationParam: {} }],
agentFrameworkContext,
agentFrameworkConfig: {
plugins: [{ toolId: 'wikiOperation', wikiOperationParam: {} }],
},
response: {
status: 'done' as const,
@ -428,18 +428,18 @@ describe('wikiOperationPlugin', () => {
};
await hooks.responseComplete.promise({
handlerContext,
pluginConfig: context.handlerConfig?.plugins?.[0] as unknown as IPromptConcatPlugin,
handlerConfig: context.handlerConfig as { plugins?: Array<{ pluginId: string; [key: string]: unknown }> },
agentFrameworkContext,
toolConfig: context.agentFrameworkConfig?.plugins?.[0] as unknown as IPromptConcatTool,
agentFrameworkConfig: context.agentFrameworkConfig as { plugins?: Array<{ toolId: string; [key: string]: unknown }> },
response: { requestId: 'r-none', content: context.response.content, status: 'done' } as AIStreamResponse,
actions: {} as PluginActions,
actions: {} as ToolActions,
requestId: 'r-none',
isFinal: true,
});
const wikiLocalAssert = container.get<Partial<IWikiService>>(serviceIdentifier.Wiki);
expect(wikiLocalAssert.wikiOperationInServer).not.toHaveBeenCalled();
expect(handlerContext.agent.messages).toHaveLength(0);
expect(agentFrameworkContext.agent.messages).toHaveLength(0);
});
});
});

View file

@ -13,15 +13,15 @@ import type { AIResponseContext, YieldNextRoundTarget } from '../types';
import { WikiChannel } from '@/constants/channels';
import serviceIdentifier from '@services/serviceIdentifier';
import type { AgentHandlerContext } from '@services/agentInstance/buildInAgentHandlers/type';
import type { AgentFrameworkContext } from '@services/agentInstance/agentFrameworks/utilities/type';
import { AgentPromptDescription } from '@services/agentInstance/promptConcat/promptConcatSchema';
import type { IPrompt } from '@services/agentInstance/promptConcat/promptConcatSchema';
import type { IPromptConcatPlugin } from '@services/agentInstance/promptConcat/promptConcatSchema';
import type { IPromptConcatTool } from '@services/agentInstance/promptConcat/promptConcatSchema';
import { cloneDeep } from 'lodash';
import defaultAgents from '../../buildInAgentHandlers/defaultAgents.json';
import { createHandlerHooks, PromptConcatHookContext } from '../index';
import { messageManagementPlugin } from '../messageManagementPlugin';
import { wikiSearchPlugin } from '../wikiSearchPlugin';
import defaultAgents from '../../agentFrameworks/taskAgents.json';
import { createAgentFrameworkHooks, PromptConcatHookContext } from '../index';
import { messageManagementTool } from '../messageManagement';
import { wikiSearchTool } from '../wikiSearch';
// Mock i18n
vi.mock('@services/libs/i18n', () => ({
@ -53,7 +53,7 @@ vi.mock('@services/libs/i18n', () => ({
// Use the real agent config
const exampleAgent = defaultAgents[0];
const handlerConfig = exampleAgent.handlerConfig as AgentPromptDescription['handlerConfig'];
const agentFrameworkConfig = (exampleAgent.agentFrameworkConfig || {}) as AgentPromptDescription['agentFrameworkConfig'];
// Services will be retrieved from container on demand inside each test/describe
@ -77,7 +77,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
it('should inject wiki tools into prompts when configured', async () => {
// Find the wiki search plugin config, make sure our default config
const wikiPlugin = handlerConfig.plugins.find((p: unknown): p is IPromptConcatPlugin => (p as IPromptConcatPlugin).pluginId === 'wikiSearch');
const wikiPlugin = agentFrameworkConfig.plugins.find((p: unknown): p is IPromptConcatTool => (p as IPromptConcatTool).toolId === 'wikiSearch');
expect(wikiPlugin).toBeDefined();
if (!wikiPlugin) {
// throw error to keep ts believe the plugin exists
@ -89,7 +89,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
expect(wikiPlugin.wikiSearchParam?.toolListPosition).toBeDefined();
// Create a copy of prompts to test modification
const prompts = cloneDeep(handlerConfig.prompts);
const prompts = cloneDeep(agentFrameworkConfig.prompts);
const messages = [
{
id: 'user-1',
@ -102,19 +102,19 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
];
const context: PromptConcatHookContext = {
handlerContext: {
agentFrameworkContext: {
agent: { id: 'test', messages: [], agentDefId: 'test', status: { state: 'working' as const, modified: new Date() }, created: new Date() },
agentDef: { id: 'test', name: 'test', handlerConfig: {} },
agentDef: { id: 'test', name: 'test', agentFrameworkConfig: {} },
isCancelled: () => false,
},
pluginConfig: wikiPlugin,
toolConfig: wikiPlugin,
prompts: prompts,
messages,
};
// Use real hooks from the plugin system
const promptHooks = createHandlerHooks();
wikiSearchPlugin(promptHooks);
const promptHooks = createAgentFrameworkHooks();
wikiSearchTool(promptHooks);
// Execute the processPrompts hook
await promptHooks.processPrompts.promise(context);
@ -130,7 +130,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
// Create a plugin config with trigger that won't match
const wikiPlugin = {
id: 'test-wiki-plugin',
pluginId: 'wikiSearch' as const,
toolId: 'wikiSearch' as const,
forbidOverrides: false,
retrievalAugmentedGenerationParam: {
sourceType: 'wiki' as const,
@ -144,11 +144,11 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
},
};
const prompts = cloneDeep(defaultAgents[0].handlerConfig.prompts);
const prompts = cloneDeep(defaultAgents[0].agentFrameworkConfig.prompts);
const originalPromptsText = JSON.stringify(prompts);
const context = {
pluginConfig: wikiPlugin,
toolConfig: wikiPlugin,
prompts,
messages: [
{
@ -162,10 +162,10 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
],
};
const hooks = createHandlerHooks();
wikiSearchPlugin(hooks);
const hooks = createAgentFrameworkHooks();
wikiSearchTool(hooks);
// build a minimal PromptConcatHookContext to run the plugin's processPrompts
const handlerCtx: AgentHandlerContext = {
const handlerCtx: AgentFrameworkContext = {
agent: {
id: 'test',
agentDefId: 'test',
@ -173,12 +173,12 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
status: { state: 'working' as const, modified: new Date() },
created: new Date(),
} as AgentInstance,
agentDef: { id: 'test', name: 'test', handlerConfig: {} } as AgentDefinition,
agentDef: { id: 'test', name: 'test', agentFrameworkConfig: {} } as AgentDefinition,
isCancelled: () => false,
};
const hookContext: PromptConcatHookContext = {
handlerContext: handlerCtx,
pluginConfig: wikiPlugin as IPromptConcatPlugin,
agentFrameworkContext: handlerCtx,
toolConfig: wikiPlugin as IPromptConcatTool,
prompts: prompts as IPrompt[],
messages: context.messages as AgentInstanceMessage[],
};
@ -221,12 +221,12 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
});
it('should execute wiki search with correct duration=1 and trigger next round', async () => {
// Find the real wikiSearch plugin config from defaultAgents.json
const wikiPlugin = handlerConfig.plugins.find((p: unknown): p is IPromptConcatPlugin => (p as IPromptConcatPlugin).pluginId === 'wikiSearch');
// Find the real wikiSearch plugin config from taskAgents.json
const wikiPlugin = agentFrameworkConfig.plugins.find((p: unknown): p is IPromptConcatTool => (p as IPromptConcatTool).toolId === 'wikiSearch');
expect(wikiPlugin).toBeDefined();
expect(wikiPlugin!.wikiSearchParam).toBeDefined();
const handlerContext = {
const agentFrameworkContext = {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -258,7 +258,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
},
],
},
agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} },
agentDef: { id: 'test-agent-def', name: 'test', agentFrameworkConfig: {} },
isCancelled: () => false,
};
@ -270,11 +270,11 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
};
const context = {
handlerContext,
agentFrameworkContext,
response,
requestId: 'test-request-123',
isFinal: true,
pluginConfig: wikiPlugin!,
toolConfig: wikiPlugin!,
prompts: [],
messages: [],
llmResponse: response.content,
@ -283,10 +283,10 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
};
// Use real handler hooks
const hooks = createHandlerHooks();
const hooks = createAgentFrameworkHooks();
// Register the plugin
wikiSearchPlugin(hooks);
wikiSearchTool(hooks);
// Execute the response complete hook
await hooks.responseComplete.promise(context);
@ -307,15 +307,15 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
);
// Check that AI tool call message now has duration=1 (should gray out immediately)
const aiToolCallMessage = handlerContext.agent.messages[1] as AgentInstanceMessage;
const aiToolCallMessage = agentFrameworkContext.agent.messages[1] as AgentInstanceMessage;
expect(aiToolCallMessage.id).toBe('ai-tool-call-msg');
expect(aiToolCallMessage.duration).toBe(1); // Should be 1 to gray out immediately
expect(aiToolCallMessage.metadata?.containsToolCall).toBe(true);
expect(aiToolCallMessage.metadata?.toolId).toBe('wiki-search');
// Verify tool result message was added to agent history with correct settings
expect(handlerContext.agent.messages.length).toBe(3); // user + ai + tool_result
const toolResultMessage = handlerContext.agent.messages[2] as AgentInstanceMessage;
expect(agentFrameworkContext.agent.messages.length).toBe(3); // user + ai + tool_result
const toolResultMessage = agentFrameworkContext.agent.messages[2] as AgentInstanceMessage;
expect(toolResultMessage.role).toBe('tool'); // Tool result message
expect(toolResultMessage.content).toContain('<functions_result>');
expect(toolResultMessage.content).toContain('Tool: wiki-search');
@ -325,13 +325,13 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
expect(toolResultMessage.duration).toBe(1); // Tool result uses configurable toolResultDuration (default 1)
// Check that previous user message is unchanged
const userMessage = handlerContext.agent.messages[0] as AgentInstanceMessage;
const userMessage = agentFrameworkContext.agent.messages[0] as AgentInstanceMessage;
expect(userMessage.id).toBe('user-msg-1');
expect(userMessage.duration).toBeUndefined(); // Should stay visible
});
it('should handle wiki search errors gracefully and set duration=1 for both messages', async () => {
const handlerContext = {
const agentFrameworkContext = {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -352,7 +352,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
},
],
},
agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} },
agentDef: { id: 'test-agent-def', name: 'test', agentFrameworkConfig: {} },
isCancelled: () => false,
};
@ -364,13 +364,13 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
};
const context = {
handlerContext,
agentFrameworkContext,
response,
requestId: 'test-request-error',
isFinal: true,
pluginConfig: {
toolConfig: {
id: 'test-plugin',
pluginId: 'wikiSearch' as const,
toolId: 'wikiSearch' as const,
forbidOverrides: false,
},
prompts: [],
@ -383,8 +383,8 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
},
};
const hooks = createHandlerHooks();
wikiSearchPlugin(hooks);
const hooks = createAgentFrameworkHooks();
wikiSearchTool(hooks);
await hooks.responseComplete.promise(context);
@ -392,14 +392,14 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
expect(context.actions.yieldNextRoundTo).toBe('self');
// Check that AI tool call message has duration=1 even after error (should gray out immediately)
const aiToolCallMessage = handlerContext.agent.messages[0] as AgentInstanceMessage;
const aiToolCallMessage = agentFrameworkContext.agent.messages[0] as AgentInstanceMessage;
expect(aiToolCallMessage.id).toBe('ai-error-tool-call');
expect(aiToolCallMessage.duration).toBe(1); // Should be 1 to gray out immediately
expect(aiToolCallMessage.metadata?.containsToolCall).toBe(true);
// Verify error message was added to agent history
expect(handlerContext.agent.messages.length).toBe(2); // tool_call + error_result
const errorResultMessage = handlerContext.agent.messages[1] as AgentInstanceMessage;
expect(agentFrameworkContext.agent.messages.length).toBe(2); // tool_call + error_result
const errorResultMessage = agentFrameworkContext.agent.messages[1] as AgentInstanceMessage;
expect(errorResultMessage.role).toBe('tool'); // Tool error message
expect(errorResultMessage.content).toContain('<functions_result>');
expect(errorResultMessage.content).toContain('Error:');
@ -411,7 +411,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
});
it('should not modify duration of unrelated messages', async () => {
const handlerContext = {
const agentFrameworkContext = {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -450,7 +450,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
},
],
},
agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} },
agentDef: { id: 'test-agent-def', name: 'test', agentFrameworkConfig: {} },
isCancelled: () => false,
};
@ -461,13 +461,13 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
};
const context = {
handlerContext,
agentFrameworkContext,
response,
requestId: 'test-request-selective',
isFinal: true,
pluginConfig: {
toolConfig: {
id: 'test-plugin',
pluginId: 'wikiSearch' as const,
toolId: 'wikiSearch' as const,
forbidOverrides: false,
},
prompts: [],
@ -480,20 +480,20 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
},
};
const hooks = createHandlerHooks();
wikiSearchPlugin(hooks);
const hooks = createAgentFrameworkHooks();
wikiSearchTool(hooks);
await hooks.responseComplete.promise(context);
// Check that unrelated messages were not modified
const unrelatedUserMsg = handlerContext.agent.messages[0] as AgentInstanceMessage;
const unrelatedUserMsg = agentFrameworkContext.agent.messages[0] as AgentInstanceMessage;
expect(unrelatedUserMsg.duration).toBe(5); // Should remain unchanged
const unrelatedAiMsg = handlerContext.agent.messages[1] as AgentInstanceMessage;
const unrelatedAiMsg = agentFrameworkContext.agent.messages[1] as AgentInstanceMessage;
expect(unrelatedAiMsg.duration).toBeUndefined(); // Should remain unchanged
// Check that only the tool call message was modified
const toolCallMsg = handlerContext.agent.messages[2] as AgentInstanceMessage;
const toolCallMsg = agentFrameworkContext.agent.messages[2] as AgentInstanceMessage;
expect(toolCallMsg.duration).toBe(1); // Should be set to 1
expect(toolCallMsg.metadata?.containsToolCall).toBe(true);
});
@ -507,20 +507,20 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
status: { state: 'working' as const, modified: new Date() },
created: new Date(),
} as AgentInstance,
agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} } as AgentDefinition,
agentDef: { id: 'test-agent-def', name: 'test', agentFrameworkConfig: {} } as AgentDefinition,
isCancelled: () => false,
};
const context: AIResponseContext = {
handlerContext: handlerCtx,
pluginConfig: { id: 'test-plugin', pluginId: 'wikiSearch' } as IPromptConcatPlugin,
agentFrameworkContext: handlerCtx,
toolConfig: { id: 'test-plugin', toolId: 'wikiSearch' } as IPromptConcatTool,
response: { requestId: 'test-request-345', content: 'Just a regular response without any tool calls', status: 'done' },
requestId: 'test-request',
isFinal: true,
};
const hooks = createHandlerHooks();
wikiSearchPlugin(hooks);
const hooks = createAgentFrameworkHooks();
wikiSearchTool(hooks);
await hooks.responseComplete.promise(context);
@ -597,7 +597,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
}) as unknown as IWikiService['wikiOperationInServer'],
);
const handlerContext = {
const agentFrameworkContext = {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -627,7 +627,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
},
],
},
agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} },
agentDef: { id: 'test-agent-def', name: 'test', agentFrameworkConfig: {} },
isCancelled: () => false,
};
@ -638,13 +638,13 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
};
const context = {
handlerContext,
agentFrameworkContext,
response,
requestId: 'test-request-vector',
requestId: 'test-request-vector-error',
isFinal: true,
pluginConfig: {
toolConfig: {
id: 'test-plugin',
pluginId: 'wikiSearch' as const,
toolId: 'wikiSearch' as const,
forbidOverrides: false,
},
prompts: [],
@ -654,8 +654,8 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
actions: {} as ActionBag,
};
const hooks = createHandlerHooks();
wikiSearchPlugin(hooks);
const hooks = createAgentFrameworkHooks();
wikiSearchTool(hooks);
await hooks.responseComplete.promise(context);
@ -676,9 +676,9 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
// Verify results were processed
expect(context.actions.yieldNextRoundTo).toBe('self');
expect(handlerContext.agent.messages.length).toBe(2);
expect(agentFrameworkContext.agent.messages.length).toBe(2);
const toolResultMessage = handlerContext.agent.messages[1] as AgentInstanceMessage;
const toolResultMessage = agentFrameworkContext.agent.messages[1] as AgentInstanceMessage;
expect(toolResultMessage.content).toContain('<functions_result>');
expect(toolResultMessage.content).toContain('Vector Result 1');
expect(toolResultMessage.content).toContain('Vector Result 2');
@ -696,7 +696,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
new Error('Vector database not initialized'),
);
const handlerContext = {
const agentFrameworkContext = {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -724,7 +724,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
},
],
},
agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} },
agentDef: { id: 'test-agent-def', name: 'test', agentFrameworkConfig: {} },
isCancelled: () => false,
};
@ -735,13 +735,13 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
};
const context = {
handlerContext,
agentFrameworkContext,
response,
requestId: 'test-request-vector-error',
isFinal: true,
pluginConfig: {
toolConfig: {
id: 'test-plugin',
pluginId: 'wikiSearch' as const,
toolId: 'wikiSearch' as const,
forbidOverrides: false,
},
prompts: [],
@ -751,15 +751,15 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
actions: {} as ActionBag,
};
const hooks = createHandlerHooks();
wikiSearchPlugin(hooks);
const hooks = createAgentFrameworkHooks();
wikiSearchTool(hooks);
await hooks.responseComplete.promise(context);
// Should still set up next round with error message
expect(context.actions.yieldNextRoundTo).toBe('self');
const errorResultMessage = handlerContext.agent.messages[1] as AgentInstanceMessage;
const errorResultMessage = agentFrameworkContext.agent.messages[1] as AgentInstanceMessage;
expect(errorResultMessage.content).toContain('Error:');
// Error message contains i18n key or actual error
expect(errorResultMessage.content).toMatch(/Vector database not initialized|Tool\.WikiSearch\.Error\.VectorSearchFailed/);
@ -767,7 +767,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
});
it('should require query parameter for vector search', async () => {
const handlerContext = {
const agentFrameworkContext = {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -795,7 +795,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
},
],
},
agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} },
agentDef: { id: 'test-agent-def', name: 'test', agentFrameworkConfig: {} },
isCancelled: () => false,
};
@ -806,13 +806,13 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
};
const context = {
handlerContext,
agentFrameworkContext,
response,
requestId: 'test-request-no-query',
isFinal: true,
pluginConfig: {
toolConfig: {
id: 'test-plugin',
pluginId: 'wikiSearch' as const,
toolId: 'wikiSearch' as const,
forbidOverrides: false,
},
prompts: [],
@ -822,13 +822,13 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
actions: {} as ActionBag,
};
const hooks = createHandlerHooks();
wikiSearchPlugin(hooks);
const hooks = createAgentFrameworkHooks();
wikiSearchTool(hooks);
await hooks.responseComplete.promise(context);
// Should return error about missing query
const errorMessage = handlerContext.agent.messages[1] as AgentInstanceMessage;
const errorMessage = agentFrameworkContext.agent.messages[1] as AgentInstanceMessage;
expect(errorMessage.content).toContain('Error:');
// Error message contains i18n key or translated text
expect(errorMessage.content).toMatch(/query|Tool\.WikiSearch\.Error\.VectorSearchRequiresQuery/);
@ -836,9 +836,9 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
});
describe('Message Persistence Integration', () => {
it('should work with messageManagementPlugin for complete persistence flow', async () => {
// This test ensures wikiSearchPlugin works well with messageManagementPlugin
const handlerContext = {
it('should work with messageManagementTool for complete persistence flow', async () => {
// This test ensures wikiSearchTool works well with messageManagementTool
const agentFrameworkContext = {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -859,7 +859,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
},
],
},
agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} },
agentDef: { id: 'test-agent-def', name: 'test', agentFrameworkConfig: {} },
isCancelled: () => false,
};
@ -870,13 +870,13 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
};
const context = {
handlerContext,
agentFrameworkContext,
response,
requestId: 'test-request-integration',
isFinal: true,
pluginConfig: {
toolConfig: {
id: 'test-plugin',
pluginId: 'wikiSearch' as const,
toolId: 'wikiSearch' as const,
forbidOverrides: false,
},
prompts: [],
@ -889,19 +889,19 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
},
};
const hooks = createHandlerHooks();
wikiSearchPlugin(hooks);
messageManagementPlugin(hooks);
const hooks = createAgentFrameworkHooks();
wikiSearchTool(hooks);
messageManagementTool(hooks);
await hooks.responseComplete.promise(context);
// Verify integration works
expect(context.actions.yieldNextRoundTo).toBe('self');
expect(handlerContext.agent.messages.length).toBe(2); // original + tool result
expect(agentFrameworkContext.agent.messages.length).toBe(2); // original + tool result
const toolResultMessage = handlerContext.agent.messages[1] as AgentInstanceMessage;
const toolResultMessage = agentFrameworkContext.agent.messages[1] as AgentInstanceMessage;
expect(toolResultMessage.metadata?.isToolResult).toBe(true);
expect(toolResultMessage.metadata?.isPersisted).toBe(true); // Should be true after messageManagementPlugin processing
expect(toolResultMessage.metadata?.isPersisted).toBe(true); // Should be true after messageManagementTool processing
});
it('should prevent regression: tool result not filtered in second round', async () => {

View file

@ -1,22 +1,22 @@
/**
* Tests for workspacesListPlugin
* Tests for workspacesListTool
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Note: global mocks from src/__tests__/setup-vitest.ts provide container and logger
import type { IPromptConcatPlugin } from '@services/agentInstance/promptConcat/promptConcatSchema';
import type { IPromptConcatTool } from '@services/agentInstance/promptConcat/promptConcatSchema';
import { container } from '@services/container';
import { logger } from '@services/libs/log';
import serviceIdentifier from '@services/serviceIdentifier';
import type { AgentHandlerContext } from '../../buildInAgentHandlers/type';
import type { AgentFrameworkContext } from '../../agentFrameworks/utilities/type';
import type { AgentInstance } from '../../interface';
import type { PromptConcatHookContext } from '../types';
import type { IWorkspaceService } from '@services/workspaces/interface';
import { createHandlerHooks } from '../index';
import { workspacesListPlugin } from '../workspacesListPlugin';
import { createAgentFrameworkHooks } from '../index';
import { workspacesListTool } from '../workspacesList';
describe('workspacesListPlugin', () => {
describe('workspacesListTool', () => {
beforeEach(async () => {
vi.clearAllMocks();
});
@ -27,11 +27,11 @@ describe('workspacesListPlugin', () => {
describe('workspaces list injection', () => {
it('should inject workspaces list when plugin is configured', async () => {
const hooks = createHandlerHooks();
workspacesListPlugin(hooks);
const hooks = createAgentFrameworkHooks();
workspacesListTool(hooks);
const context: PromptConcatHookContext = {
handlerContext: {
agentFrameworkContext: {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -41,7 +41,7 @@ describe('workspacesListPlugin', () => {
} as AgentInstance,
agentDef: { id: 'test-agent-def', name: 'test-agent-def' } as unknown,
isCancelled: () => false,
} as AgentHandlerContext,
} as AgentFrameworkContext,
messages: [],
prompts: [
{
@ -50,16 +50,16 @@ describe('workspacesListPlugin', () => {
children: [],
},
],
pluginConfig: {
toolConfig: {
id: 'test-plugin',
caption: 'Test Plugin',
forbidOverrides: false,
pluginId: 'workspacesList',
toolId: 'workspacesList',
workspacesListParam: {
targetId: 'target-prompt',
position: 'after' as const,
},
} as unknown as IPromptConcatPlugin,
} as unknown as IPromptConcatTool,
};
await hooks.processPrompts.promise(context);
@ -74,11 +74,11 @@ describe('workspacesListPlugin', () => {
});
it('should inject workspaces list when position is before', async () => {
const hooks = createHandlerHooks();
workspacesListPlugin(hooks);
const hooks = createAgentFrameworkHooks();
workspacesListTool(hooks);
const context: PromptConcatHookContext = {
handlerContext: {
agentFrameworkContext: {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -88,7 +88,7 @@ describe('workspacesListPlugin', () => {
} as AgentInstance,
agentDef: { id: 'test-agent-def', name: 'test-agent-def' } as unknown,
isCancelled: () => false,
} as AgentHandlerContext,
} as AgentFrameworkContext,
messages: [],
prompts: [
{
@ -97,16 +97,16 @@ describe('workspacesListPlugin', () => {
children: [],
},
],
pluginConfig: {
toolConfig: {
id: 'test-plugin',
caption: 'Test Plugin',
forbidOverrides: false,
pluginId: 'workspacesList',
toolId: 'workspacesList',
workspacesListParam: {
targetId: 'target-prompt',
position: 'before' as const,
},
} as unknown as IPromptConcatPlugin,
} as unknown as IPromptConcatTool,
};
await hooks.processPrompts.promise(context);
@ -118,11 +118,11 @@ describe('workspacesListPlugin', () => {
});
it('should not inject content when plugin is not configured', async () => {
const hooks = createHandlerHooks();
workspacesListPlugin(hooks);
const hooks = createAgentFrameworkHooks();
workspacesListTool(hooks);
const context: PromptConcatHookContext = {
handlerContext: {
agentFrameworkContext: {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -132,7 +132,7 @@ describe('workspacesListPlugin', () => {
} as AgentInstance,
agentDef: { id: 'test-agent-def', name: 'test-agent-def' } as unknown,
isCancelled: () => false,
} as AgentHandlerContext,
} as AgentFrameworkContext,
messages: [],
prompts: [
{
@ -141,7 +141,7 @@ describe('workspacesListPlugin', () => {
children: [],
},
],
pluginConfig: { id: 'test-plugin', pluginId: 'otherPlugin', forbidOverrides: false } as unknown as IPromptConcatPlugin,
toolConfig: { id: 'test-plugin', toolId: 'otherPlugin', forbidOverrides: false } as unknown as IPromptConcatTool,
};
await hooks.processPrompts.promise(context);
@ -155,11 +155,11 @@ describe('workspacesListPlugin', () => {
const workspaceService = container.get<Partial<IWorkspaceService>>(serviceIdentifier.Workspace);
workspaceService.getWorkspacesAsList = vi.fn().mockResolvedValue([]) as unknown as IWorkspaceService['getWorkspacesAsList'];
const hooks = createHandlerHooks();
workspacesListPlugin(hooks);
const hooks = createAgentFrameworkHooks();
workspacesListTool(hooks);
const context: PromptConcatHookContext = {
handlerContext: {
agentFrameworkContext: {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -169,7 +169,7 @@ describe('workspacesListPlugin', () => {
} as AgentInstance,
agentDef: { id: 'test-agent-def', name: 'test-agent-def' } as unknown,
isCancelled: () => false,
} as AgentHandlerContext,
} as AgentFrameworkContext,
messages: [],
prompts: [
{
@ -178,16 +178,16 @@ describe('workspacesListPlugin', () => {
children: [],
},
],
pluginConfig: {
toolConfig: {
id: 'test-plugin',
caption: 'Test Plugin',
forbidOverrides: false,
pluginId: 'workspacesList',
toolId: 'workspacesList',
workspacesListParam: {
targetId: 'target-prompt',
position: 'after' as const,
},
} as unknown as IPromptConcatPlugin,
} as unknown as IPromptConcatTool,
};
await hooks.processPrompts.promise(context);
@ -195,16 +195,16 @@ describe('workspacesListPlugin', () => {
const targetPrompt = context.prompts[0];
expect(targetPrompt.children).toHaveLength(0);
expect(logger.debug).toHaveBeenCalledWith('No wiki workspaces found to inject', {
pluginId: 'test-plugin',
toolId: 'test-plugin',
});
});
it('should warn when target prompt is not found', async () => {
const hooks = createHandlerHooks();
workspacesListPlugin(hooks);
const hooks = createAgentFrameworkHooks();
workspacesListTool(hooks);
const context: PromptConcatHookContext = {
handlerContext: {
agentFrameworkContext: {
agent: {
id: 'test-agent',
agentDefId: 'test-agent-def',
@ -214,7 +214,7 @@ describe('workspacesListPlugin', () => {
} as AgentInstance,
agentDef: { id: 'test-agent-def', name: 'test-agent-def' } as unknown,
isCancelled: () => false,
} as AgentHandlerContext,
} as AgentFrameworkContext,
messages: [],
prompts: [
{
@ -223,23 +223,23 @@ describe('workspacesListPlugin', () => {
children: [],
},
],
pluginConfig: {
toolConfig: {
id: 'test-plugin',
caption: 'Test Plugin',
forbidOverrides: false,
pluginId: 'workspacesList',
toolId: 'workspacesList',
workspacesListParam: {
targetId: 'non-existent-prompt',
position: 'after' as const,
},
} as unknown as IPromptConcatPlugin,
} as unknown as IPromptConcatTool,
};
await hooks.processPrompts.promise(context);
expect(logger.warn).toHaveBeenCalledWith('Workspaces list target prompt not found', {
targetId: 'non-existent-prompt',
pluginId: 'test-plugin',
toolId: 'test-plugin',
});
});
});

View file

@ -0,0 +1,190 @@
import { logger } from '@services/libs/log';
import { AsyncSeriesHook, AsyncSeriesWaterfallHook } from 'tapable';
import { registerToolParameterSchema } from './schemaRegistry';
import { AgentResponse, PromptConcatHookContext, PromptConcatHooks, PromptConcatTool, ResponseHookContext } from './types';
// Re-export types for convenience
export type { AgentResponse, PromptConcatHookContext, PromptConcatHooks, PromptConcatTool, ResponseHookContext };
// Backward compatibility aliases
export type { PromptConcatTool as PromptConcatPlugin };
/**
* Registry for built-in framework tools
*/
export const builtInTools = new Map<string, PromptConcatTool>();
/**
* Create unified hooks instance for the complete agent framework tool system
*/
export function createAgentFrameworkHooks(): PromptConcatHooks {
return {
// Prompt processing hooks
processPrompts: new AsyncSeriesWaterfallHook(['context']),
finalizePrompts: new AsyncSeriesWaterfallHook(['context']),
postProcess: new AsyncSeriesWaterfallHook(['context']),
// Agent lifecycle hooks
userMessageReceived: new AsyncSeriesHook(['context']),
agentStatusChanged: new AsyncSeriesHook(['context']),
toolExecuted: new AsyncSeriesHook(['context']),
responseUpdate: new AsyncSeriesHook(['context']),
responseComplete: new AsyncSeriesHook(['context']),
};
}
/**
* Get all available tools
*/
async function getAllTools() {
const [
promptToolsModule,
wikiSearchModule,
wikiOperationModule,
workspacesListModule,
messageManagementModule,
] = await Promise.all([
import('./prompt'),
import('./wikiSearch'),
import('./wikiOperation'),
import('./workspacesList'),
import('./messageManagement'),
]);
return {
messageManagement: messageManagementModule.messageManagementTool,
fullReplacement: promptToolsModule.fullReplacementTool,
wikiSearch: wikiSearchModule.wikiSearchTool,
wikiOperation: wikiOperationModule.wikiOperationTool,
workspacesList: workspacesListModule.workspacesListTool,
};
}
/**
* Register tools to hooks based on framework configuration
* @param hooks - The hooks instance to register tools to
* @param agentFrameworkConfig - The framework configuration containing tool settings
*/
export async function registerToolsToHooksFromConfig(
hooks: PromptConcatHooks,
agentFrameworkConfig: { plugins?: Array<{ toolId: string; [key: string]: unknown }> },
): Promise<void> {
// Always register core tools that are needed for basic functionality
const messageManagementModule = await import('./messageManagement');
messageManagementModule.messageManagementTool(hooks);
logger.debug('Registered messageManagementTool to hooks');
// Register tools based on framework configuration
if (agentFrameworkConfig.plugins) {
for (const toolConfig of agentFrameworkConfig.plugins) {
const { toolId } = toolConfig;
// Get tool from global registry (supports both built-in and dynamic tools)
const tool = builtInTools.get(toolId);
if (tool) {
tool(hooks);
logger.debug(`Registered tool ${toolId} to hooks`);
} else {
logger.warn(`Tool not found in registry: ${toolId}`);
}
}
}
}
/**
* Initialize tool system - register all built-in tools to global registry
* This should be called once during service initialization
*/
export async function initializeToolSystem(): Promise<void> {
// Import tool schemas and register them
const [
promptToolsModule,
wikiSearchModule,
wikiOperationModule,
workspacesListModule,
modelContextProtocolModule,
] = await Promise.all([
import('./prompt'),
import('./wikiSearch'),
import('./wikiOperation'),
import('./workspacesList'),
import('./modelContextProtocol'),
]);
// Register tool parameter schemas
registerToolParameterSchema(
'fullReplacement',
promptToolsModule.getFullReplacementParameterSchema(),
{
displayName: 'Full Replacement',
description: 'Replace target content with content from specified source',
},
);
registerToolParameterSchema(
'dynamicPosition',
promptToolsModule.getDynamicPositionParameterSchema(),
{
displayName: 'Dynamic Position',
description: 'Insert content at a specific position relative to a target element',
},
);
registerToolParameterSchema(
'wikiSearch',
wikiSearchModule.getWikiSearchParameterSchema(),
{
displayName: 'Wiki Search',
description: 'Search content in wiki workspaces and manage vector embeddings',
},
);
registerToolParameterSchema(
'wikiOperation',
wikiOperationModule.getWikiOperationParameterSchema(),
{
displayName: 'Wiki Operation',
description: 'Perform operations on wiki workspaces (create, update, delete tiddlers)',
},
);
registerToolParameterSchema(
'workspacesList',
workspacesListModule.getWorkspacesListParameterSchema(),
{
displayName: 'Workspaces List',
description: 'Inject available wiki workspaces list into prompts',
},
);
registerToolParameterSchema(
'modelContextProtocol',
modelContextProtocolModule.getModelContextProtocolParameterSchema(),
{
displayName: 'Model Context Protocol',
description: 'MCP (Model Context Protocol) integration',
},
);
const tools = await getAllTools();
// Register all built-in tools to global registry for discovery
builtInTools.set('messageManagement', tools.messageManagement);
builtInTools.set('fullReplacement', tools.fullReplacement);
builtInTools.set('wikiSearch', tools.wikiSearch);
builtInTools.set('wikiOperation', tools.wikiOperation);
builtInTools.set('workspacesList', tools.workspacesList);
logger.debug('All built-in tools and schemas registered successfully');
}
/**
* Create hooks and register tools based on framework configuration
* This creates a new hooks instance and registers tools for that specific context
*/
export async function createHooksWithTools(
agentFrameworkConfig: { plugins?: Array<{ toolId: string; [key: string]: unknown }> },
): Promise<{ hooks: PromptConcatHooks; toolConfigs: Array<{ toolId: string; [key: string]: unknown }> }> {
const hooks = createAgentFrameworkHooks();
await registerToolsToHooksFromConfig(hooks, agentFrameworkConfig);
return {
hooks,
toolConfigs: agentFrameworkConfig.plugins || [],
};
}

View file

@ -8,20 +8,20 @@ import { logger } from '@services/libs/log';
import serviceIdentifier from '@services/serviceIdentifier';
import type { IAgentInstanceService } from '../interface';
import { createAgentMessage } from '../utilities';
import type { AgentStatusContext, AIResponseContext, PromptConcatPlugin, ToolExecutionContext, UserMessageContext } from './types';
import type { AgentStatusContext, AIResponseContext, PromptConcatTool, ToolExecutionContext, UserMessageContext } from './types';
/**
* Message management plugin
* Handles all message-related operations: persistence, streaming, UI updates, and duration-based filtering
*/
export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
export const messageManagementTool: PromptConcatTool = (hooks) => {
// Handle user message persistence
hooks.userMessageReceived.tapAsync('messageManagementPlugin', async (context: UserMessageContext, callback) => {
hooks.userMessageReceived.tapAsync('messageManagementTool', async (context: UserMessageContext, callback) => {
try {
const { handlerContext, content, messageId } = context;
const { agentFrameworkContext, content, messageId } = context;
// Create user message using the helper function
const userMessage = createAgentMessage(messageId, handlerContext.agent.id, {
const userMessage = createAgentMessage(messageId, agentFrameworkContext.agent.id, {
role: 'user',
content: content.text,
contentType: 'text/plain',
@ -30,7 +30,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
});
// Add message to the agent's message array for immediate use (do this before persistence so plugins see it)
handlerContext.agent.messages.push(userMessage);
agentFrameworkContext.agent.messages.push(userMessage);
// Get the agent instance service to access repositories
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
@ -40,7 +40,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
logger.debug('User message persisted to database', {
messageId,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
contentLength: content.text.length,
});
@ -49,30 +49,30 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
logger.error('Message management plugin error in userMessageReceived', {
error,
messageId: context.messageId,
agentId: context.handlerContext.agent.id,
agentId: context.agentFrameworkContext.agent.id,
});
callback();
}
});
// Handle agent status persistence
hooks.agentStatusChanged.tapAsync('messageManagementPlugin', async (context: AgentStatusContext, callback) => {
hooks.agentStatusChanged.tapAsync('messageManagementTool', async (context: AgentStatusContext, callback) => {
try {
const { handlerContext, status } = context;
const { agentFrameworkContext, status } = context;
// Get the agent instance service to update status
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
// Update agent status in database
await agentInstanceService.updateAgent(handlerContext.agent.id, {
await agentInstanceService.updateAgent(agentFrameworkContext.agent.id, {
status,
});
// Update the agent object for immediate use
handlerContext.agent.status = status;
agentFrameworkContext.agent.status = status;
logger.debug('Agent status updated in database', {
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
state: status.state,
});
@ -80,7 +80,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
} catch (error) {
logger.error('Message management plugin error in agentStatusChanged', {
error,
agentId: context.handlerContext.agent.id,
agentId: context.agentFrameworkContext.agent.id,
status: context.status,
});
callback();
@ -88,13 +88,13 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
});
// Handle AI response updates during streaming
hooks.responseUpdate.tapAsync('messageManagementPlugin', async (context: AIResponseContext, callback) => {
hooks.responseUpdate.tapAsync('messageManagementTool', async (context: AIResponseContext, callback) => {
try {
const { handlerContext, response } = context;
const { agentFrameworkContext, response } = context;
if (response.status === 'update' && response.content) {
// Find or create AI response message in agent's message array
let aiMessage = handlerContext.agent.messages.find(
let aiMessage = agentFrameworkContext.agent.messages.find(
(message) => message.role === 'assistant' && !message.metadata?.isComplete,
);
@ -103,7 +103,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
const now = new Date();
aiMessage = {
id: `ai-response-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
role: 'assistant',
content: response.content,
created: now,
@ -111,7 +111,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
metadata: { isComplete: false },
duration: undefined, // AI responses persist indefinitely by default
};
handlerContext.agent.messages.push(aiMessage);
agentFrameworkContext.agent.messages.push(aiMessage);
// Persist immediately so DB timestamp reflects conversation order
try {
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
@ -132,7 +132,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
// Update UI using the agent instance service
try {
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
agentInstanceService.debounceUpdateMessage(aiMessage, handlerContext.agent.id);
agentInstanceService.debounceUpdateMessage(aiMessage, agentFrameworkContext.agent.id);
} catch (serviceError) {
logger.warn('Failed to update UI for streaming message', {
error: serviceError,
@ -150,13 +150,13 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
});
// Handle AI response completion
hooks.responseComplete.tapAsync('messageManagementPlugin', async (context: AIResponseContext, callback) => {
hooks.responseComplete.tapAsync('messageManagementTool', async (context: AIResponseContext, callback) => {
try {
const { handlerContext, response } = context;
const { agentFrameworkContext, response } = context;
if (response.status === 'done' && response.content) {
// Find and finalize AI response message
let aiMessage = handlerContext.agent.messages.find(
let aiMessage = agentFrameworkContext.agent.messages.find(
(message) => message.role === 'assistant' && !message.metadata?.isComplete && !message.metadata?.isToolResult,
);
@ -170,7 +170,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
const nowFinal = new Date();
aiMessage = {
id: `ai-response-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
role: 'assistant',
content: response.content,
created: nowFinal,
@ -180,7 +180,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
},
duration: undefined, // Default duration for AI responses
};
handlerContext.agent.messages.push(aiMessage);
agentFrameworkContext.agent.messages.push(aiMessage);
}
// Get the agent instance service for persistence and UI updates
@ -191,7 +191,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
// Final UI update
try {
agentInstanceService.debounceUpdateMessage(aiMessage, handlerContext.agent.id);
agentInstanceService.debounceUpdateMessage(aiMessage, agentFrameworkContext.agent.id);
} catch (serviceError) {
logger.warn('Failed to update UI for completed message', {
error: serviceError,
@ -215,12 +215,12 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
});
// Handle tool result messages persistence and UI updates
hooks.toolExecuted.tapAsync('messageManagementPlugin', async (context: ToolExecutionContext, callback) => {
hooks.toolExecuted.tapAsync('messageManagementTool', async (context: ToolExecutionContext, callback) => {
try {
const { handlerContext } = context;
const { agentFrameworkContext } = context;
// Find newly added tool result messages that need to be persisted
const newToolResultMessages = handlerContext.agent.messages.filter(
const newToolResultMessages = agentFrameworkContext.agent.messages.filter(
(message) => message.metadata?.isToolResult && !message.metadata.isPersisted,
);
@ -233,7 +233,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
await agentInstanceService.saveUserMessage(message);
// Update UI
agentInstanceService.debounceUpdateMessage(message, handlerContext.agent.id);
agentInstanceService.debounceUpdateMessage(message, agentFrameworkContext.agent.id);
// Mark as persisted to avoid duplicate saves
message.metadata = { ...message.metadata, isPersisted: true, uiUpdated: true };

View file

@ -10,7 +10,7 @@ import { findPromptById } from '../promptConcat/promptConcat';
import type { IPrompt } from '../promptConcat/promptConcatSchema';
import { filterMessagesByDuration } from '../utilities/messageDurationFilter';
import { normalizeRole } from '../utilities/normalizeRole';
import { AgentResponse, PromptConcatPlugin, ResponseHookContext } from './types';
import { AgentResponse, PromptConcatTool, ResponseHookContext } from './types';
const t = identity;
@ -76,17 +76,17 @@ export function getDynamicPositionParameterSchema() {
* Full replacement plugin
* Replaces target content with content from specified source
*/
export const fullReplacementPlugin: PromptConcatPlugin = (hooks) => {
export const fullReplacementTool: PromptConcatTool = (hooks) => {
// Normalize an AgentInstanceMessage role to Prompt role
hooks.processPrompts.tapAsync('fullReplacementPlugin', async (context, callback) => {
const { pluginConfig, prompts, messages } = context;
hooks.processPrompts.tapAsync('fullReplacementTool', async (context, callback) => {
const { toolConfig, prompts, messages } = context;
if (pluginConfig.pluginId !== 'fullReplacement' || !pluginConfig.fullReplacementParam) {
if (toolConfig.toolId !== 'fullReplacement' || !toolConfig.fullReplacementParam) {
callback();
return;
}
const fullReplacementConfig = pluginConfig.fullReplacementParam;
const fullReplacementConfig = toolConfig.fullReplacementParam;
if (!fullReplacementConfig) {
callback();
return;
@ -98,7 +98,7 @@ export const fullReplacementPlugin: PromptConcatPlugin = (hooks) => {
if (!found) {
logger.warn('Target prompt not found for fullReplacement', {
targetId,
pluginId: pluginConfig.id,
toolId: toolConfig.id,
});
callback();
return;
@ -173,16 +173,16 @@ export const fullReplacementPlugin: PromptConcatPlugin = (hooks) => {
});
// Handle response phase for llmResponse source type
hooks.postProcess.tapAsync('fullReplacementPlugin', async (context, callback) => {
hooks.postProcess.tapAsync('fullReplacementTool', async (context, callback) => {
const responseContext = context as ResponseHookContext;
const { pluginConfig, llmResponse, responses } = responseContext;
const { toolConfig, llmResponse, responses } = responseContext;
if (pluginConfig.pluginId !== 'fullReplacement' || !pluginConfig.fullReplacementParam) {
if (toolConfig.toolId !== 'fullReplacement' || !toolConfig.fullReplacementParam) {
callback();
return;
}
const fullReplacementParameter = pluginConfig.fullReplacementParam;
const fullReplacementParameter = toolConfig.fullReplacementParam;
if (!fullReplacementParameter) {
callback();
return;
@ -202,7 +202,7 @@ export const fullReplacementPlugin: PromptConcatPlugin = (hooks) => {
if (!found) {
logger.warn('Full replacement target not found in responses', {
targetId,
pluginId: pluginConfig.id,
toolId: toolConfig.id,
});
callback();
return;
@ -212,7 +212,7 @@ export const fullReplacementPlugin: PromptConcatPlugin = (hooks) => {
logger.debug('Replacing target with LLM response', {
targetId,
responseLength: llmResponse.length,
pluginId: pluginConfig.id,
toolId: toolConfig.id,
});
found.text = llmResponse;
@ -220,6 +220,7 @@ export const fullReplacementPlugin: PromptConcatPlugin = (hooks) => {
logger.debug('Full replacement completed in response phase', {
targetId,
sourceType,
toolId: toolConfig.id,
});
callback();
@ -230,16 +231,16 @@ export const fullReplacementPlugin: PromptConcatPlugin = (hooks) => {
* Dynamic position plugin
* Inserts content at a specific position relative to a target element
*/
export const dynamicPositionPlugin: PromptConcatPlugin = (hooks) => {
hooks.processPrompts.tapAsync('dynamicPositionPlugin', async (context, callback) => {
const { pluginConfig, prompts } = context;
export const dynamicPositionTool: PromptConcatTool = (hooks) => {
hooks.processPrompts.tapAsync('dynamicPositionTool', async (context, callback) => {
const { toolConfig, prompts } = context;
if (pluginConfig.pluginId !== 'dynamicPosition' || !pluginConfig.dynamicPositionParam || !pluginConfig.content) {
if (toolConfig.toolId !== 'dynamicPosition' || !toolConfig.dynamicPositionParam || !toolConfig.content) {
callback();
return;
}
const dynamicPositionConfig = pluginConfig.dynamicPositionParam;
const dynamicPositionConfig = toolConfig.dynamicPositionParam;
if (!dynamicPositionConfig) {
callback();
return;
@ -251,7 +252,7 @@ export const dynamicPositionPlugin: PromptConcatPlugin = (hooks) => {
if (!found) {
logger.warn('Target prompt not found for dynamicPosition', {
targetId,
pluginId: pluginConfig.id,
toolId: toolConfig.id,
});
callback();
return;
@ -259,9 +260,9 @@ export const dynamicPositionPlugin: PromptConcatPlugin = (hooks) => {
// Create new prompt part
const newPart: IPrompt = {
id: `dynamic-${pluginConfig.id}-${Date.now()}`,
caption: pluginConfig.caption || 'Dynamic Content',
text: pluginConfig.content,
id: `dynamic-${toolConfig.id}-${Date.now()}`,
caption: toolConfig.caption || 'Dynamic Content',
text: toolConfig.content,
};
// Insert based on position
@ -288,7 +289,8 @@ export const dynamicPositionPlugin: PromptConcatPlugin = (hooks) => {
logger.debug('Dynamic position insertion completed', {
targetId,
position,
contentLength: pluginConfig.content.length,
contentLength: toolConfig.content.length,
toolId: toolConfig.id,
});
callback();

View file

@ -0,0 +1,165 @@
/**
* Tool Schema Registry
*
* This system allows tools to register their parameter schemas dynamically,
* enabling dynamic tool loading while maintaining type safety and validation.
*/
import { identity } from 'lodash';
import { z } from 'zod/v4';
const t = identity;
/**
* Registry for tool parameter schemas
*/
const toolSchemas = new Map<string, z.ZodType>();
/**
* Registry for tool metadata
*/
const toolMetadata = new Map<string, {
displayName: string;
description: string;
}>();
/**
* Register a tool parameter schema
* @param toolId The tool ID (should match toolId enum values)
* @param schema The Zod schema for this tool's parameters
* @param metadata Optional metadata for display purposes
*/
export function registerToolParameterSchema(
toolId: string,
schema: z.ZodType,
metadata?: {
displayName: string;
description: string;
},
): void {
toolSchemas.set(toolId, schema);
if (metadata) {
toolMetadata.set(toolId, metadata);
}
}
/**
* Get a tool parameter schema by ID
* @param toolId The tool ID
* @returns The schema or undefined if not found
*/
export function getToolParameterSchema(toolId: string): z.ZodType | undefined {
return toolSchemas.get(toolId);
}
/**
* Get all registered tool IDs
* @returns Array of all registered tool IDs
*/
export function getAllRegisteredToolIds(): string[] {
return Array.from(toolSchemas.keys());
}
/**
* Get tool metadata
* @param toolId The tool ID
* @returns Tool metadata or undefined if not found
*/
export function getToolMetadata(toolId: string): { displayName: string; description: string } | undefined {
return toolMetadata.get(toolId);
}
/**
* Dynamically create the PromptConcatToolSchema based on registered tools
* This is called whenever the schema is needed, ensuring it includes all registered tools
*/
export function createDynamicPromptConcatToolSchema(): z.ZodType {
// Base tool configuration without parameter-specific fields
const baseToolSchema = z.object({
id: z.string().meta({
title: t('Schema.Tool.IdTitle'),
description: t('Schema.Tool.Id'),
}),
caption: z.string().optional().meta({
title: t('Schema.Tool.CaptionTitle'),
description: t('Schema.Tool.Caption'),
}),
content: z.string().optional().meta({
title: t('Schema.Tool.ContentTitle'),
description: t('Schema.Tool.Content'),
}),
forbidOverrides: z.boolean().optional().default(false).meta({
title: t('Schema.Tool.ForbidOverridesTitle'),
description: t('Schema.Tool.ForbidOverrides'),
}),
});
// Get all registered tool IDs
const registeredToolIds = getAllRegisteredToolIds();
if (registeredToolIds.length === 0) {
// Fallback to a basic schema if no tools are registered yet
return baseToolSchema.extend({
toolId: z.string().meta({
title: t('Schema.Tool.ToolIdTitle'),
description: t('Schema.Tool.ToolId'),
}),
});
}
// Create enum from registered tool IDs
const toolIdEnum = z.enum(registeredToolIds as [string, ...string[]]).meta({
title: t('Schema.Tool.ToolIdTitle'),
description: t('Schema.Tool.ToolId'),
enumOptions: registeredToolIds.map(toolId => {
const metadata = getToolMetadata(toolId);
return {
value: toolId,
label: metadata?.displayName || toolId,
};
}),
});
// Create parameter schema object with all registered tools
const parameterSchema: Record<string, z.ZodType> = {};
for (const toolId of registeredToolIds) {
const schema = getToolParameterSchema(toolId);
if (schema) {
const metadata = getToolMetadata(toolId);
parameterSchema[`${toolId}Param`] = schema.optional().meta({
title: metadata?.displayName || toolId,
description: metadata?.description || `Parameters for ${toolId} tool`,
});
}
}
// Combine base schema with tool ID and parameters
return baseToolSchema.extend({
toolId: toolIdEnum,
...parameterSchema,
});
}
/**
* Get the type of a tool's parameters
* @param toolId The tool ID
* @returns The inferred TypeScript type of the tool's parameters
*/
export type ToolParameterType<T extends string> = T extends keyof ReturnType<typeof createToolParameterTypes> ? ReturnType<typeof createToolParameterTypes>[T] : never;
/**
* Create type definitions for all registered tool parameters
* This is used internally for type inference
*/
export function createToolParameterTypes() {
const types: Record<string, unknown> = {};
for (const toolId of getAllRegisteredToolIds()) {
const schema = getToolParameterSchema(toolId);
if (schema) {
types[toolId] = schema;
}
}
return types as Record<string, z.ZodType>;
}

View file

@ -1,9 +1,9 @@
import { ToolCallingMatch } from '@services/agentDefinition/interface';
import { AgentHandlerContext } from '@services/agentInstance/buildInAgentHandlers/type';
import { AgentFrameworkContext } from '@services/agentInstance/agentFrameworks/utilities/type';
import { AgentInstanceMessage } from '@services/agentInstance/interface';
import { AIStreamResponse } from '@services/externalAPI/interface';
import { AsyncSeriesHook, AsyncSeriesWaterfallHook } from 'tapable';
import type { IPrompt, IPromptConcatPlugin } from '../promptConcat/promptConcatSchema/';
import type { IPrompt, IPromptConcatTool } from '../promptConcat/promptConcatSchema';
/**
* Next round target options
@ -11,9 +11,9 @@ import type { IPrompt, IPromptConcatPlugin } from '../promptConcat/promptConcatS
export type YieldNextRoundTarget = 'human' | 'self' | `agent:${string}`; // allows for future agent IDs like "agent:agent-id"
/**
* Unified actions interface for all plugin hooks
* Unified actions interface for all tool hooks
*/
export interface PluginActions {
export interface ToolActions {
/** Whether to yield next round to continue processing */
yieldNextRoundTo?: YieldNextRoundTarget;
/** New user message to append */
@ -23,27 +23,27 @@ export interface PluginActions {
}
/**
* Base context interface for all plugin hooks
* Base context interface for all tool hooks
*/
export interface BasePluginContext {
/** Handler context */
handlerContext: AgentHandlerContext;
export interface BaseToolContext {
/** Framework context */
agentFrameworkContext: AgentFrameworkContext;
/** Additional context data */
metadata?: Record<string, unknown>;
/** Actions set by plugins during processing */
actions?: PluginActions;
/** Actions set by tools during processing */
actions?: ToolActions;
}
/**
* Context for prompt processing hooks (processPrompts, finalizePrompts)
*/
export interface PromptConcatHookContext extends BasePluginContext {
export interface PromptConcatHookContext extends BaseToolContext {
/** Array of agent instance messages for context */
messages: AgentInstanceMessage[];
/** Current prompt tree */
prompts: IPrompt[];
/** Plugin configuration */
pluginConfig: IPromptConcatPlugin;
/** Tool configuration */
toolConfig: IPromptConcatTool;
}
/**
@ -59,11 +59,11 @@ export interface PostProcessContext extends PromptConcatHookContext {
/**
* Context for AI response hooks (responseUpdate, responseComplete)
*/
export interface AIResponseContext extends BasePluginContext {
/** Plugin configuration - for backward compatibility */
pluginConfig: IPromptConcatPlugin;
/** Complete handler configuration - allows plugins to access all configs */
handlerConfig?: { plugins?: Array<{ pluginId: string; [key: string]: unknown }> };
export interface AIResponseContext extends BaseToolContext {
/** Tool configuration - for backward compatibility */
toolConfig: IPromptConcatTool;
/** Complete framework configuration - allows tools to access all configs */
agentFrameworkConfig?: { plugins?: Array<{ toolId: string; [key: string]: unknown }> };
/** AI streaming response */
response: AIStreamResponse;
/** Current request ID */
@ -75,7 +75,7 @@ export interface AIResponseContext extends BasePluginContext {
/**
* Context for user message hooks
*/
export interface UserMessageContext extends BasePluginContext {
export interface UserMessageContext extends BaseToolContext {
/** User message content */
content: { text: string; file?: File };
/** Generated message ID */
@ -87,7 +87,7 @@ export interface UserMessageContext extends BasePluginContext {
/**
* Context for agent status hooks
*/
export interface AgentStatusContext extends BasePluginContext {
export interface AgentStatusContext extends BaseToolContext {
/** New status state */
status: {
state: 'working' | 'completed' | 'failed' | 'canceled';
@ -98,7 +98,7 @@ export interface AgentStatusContext extends BasePluginContext {
/**
* Context for tool execution hooks
*/
export interface ToolExecutionContext extends BasePluginContext {
export interface ToolExecutionContext extends BaseToolContext {
/** Tool execution result */
toolResult: {
success: boolean;
@ -136,7 +136,7 @@ export interface ResponseHookContext extends PromptConcatHookContext {
}
/**
* Handler hooks for unified plugin system
* Framework hooks for unified tool system
* Handles both prompt processing and agent lifecycle events
*/
export interface PromptConcatHooks {
@ -159,6 +159,6 @@ export interface PromptConcatHooks {
}
/**
* Universal plugin function interface - can register handlers for any hooks
* Universal tool function interface - can register handlers for any hooks
*/
export type PromptConcatPlugin = (hooks: PromptConcatHooks) => void;
export type PromptConcatTool = (hooks: PromptConcatHooks) => void;

View file

@ -16,7 +16,7 @@ import { z } from 'zod/v4';
import type { AgentInstanceMessage, IAgentInstanceService } from '../interface';
import { findPromptById } from '../promptConcat/promptConcat';
import { schemaToToolContent } from '../utilities/schemaToToolContent';
import type { PromptConcatPlugin } from './types';
import type { PromptConcatTool } from './types';
/**
* Wiki Operation Parameter Schema
@ -101,17 +101,17 @@ const WikiOperationToolParameterSchema = z.object({
* Wiki Operation plugin - Prompt processing
* Handles tool list injection for wiki operation functionality
*/
export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
export const wikiOperationTool: PromptConcatTool = (hooks) => {
// First tapAsync: Tool list injection
hooks.processPrompts.tapAsync('wikiOperationPlugin-toolList', async (context, callback) => {
const { pluginConfig, prompts } = context;
hooks.processPrompts.tapAsync('wikiOperationTool-toolList', async (context, callback) => {
const { toolConfig, prompts } = context;
if (pluginConfig.pluginId !== 'wikiOperation' || !pluginConfig.wikiOperationParam) {
if (toolConfig.toolId !== 'wikiOperation' || !toolConfig.wikiOperationParam) {
callback();
return;
}
const wikiOperationParameter = pluginConfig.wikiOperationParam;
const wikiOperationParameter = toolConfig.wikiOperationParam;
try {
// Handle tool list injection if toolListPosition is configured
@ -121,7 +121,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
if (!toolListTarget) {
logger.warn('Tool list target prompt not found', {
targetId: toolListPosition.targetId,
pluginId: pluginConfig.id,
toolId: toolConfig.id,
});
callback();
return;
@ -140,7 +140,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
}
const insertIndex = toolListTarget.prompt.children.length;
toolListTarget.prompt.children.splice(insertIndex, 0, {
id: `wiki-operation-tool-${pluginConfig.id}`,
id: `wiki-operation-tool-${toolConfig.id}`,
caption: 'Wiki Operation Tool',
text: wikiOperationToolContent,
});
@ -149,7 +149,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
toolListTarget.prompt.children = [];
}
toolListTarget.prompt.children.unshift({
id: `wiki-operation-tool-${pluginConfig.id}`,
id: `wiki-operation-tool-${toolConfig.id}`,
caption: 'Wiki Operation Tool',
text: wikiOperationToolContent,
});
@ -161,7 +161,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
logger.debug('Wiki operation tool list injected', {
targetId: toolListPosition.targetId,
position: toolListPosition.position,
pluginId: pluginConfig.id,
toolId: toolConfig.id,
});
}
@ -169,20 +169,20 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
} catch (error) {
logger.error('Error in wiki operation tool list injection', {
error,
pluginId: pluginConfig.id,
toolId: toolConfig.id,
});
callback();
}
});
// 2. Tool execution when AI response is complete
hooks.responseComplete.tapAsync('wikiOperationPlugin-handler', async (context, callback) => {
hooks.responseComplete.tapAsync('wikiOperationTool-handler', async (context, callback) => {
try {
const { handlerContext, response, handlerConfig } = context;
const { agentFrameworkContext, response, agentFrameworkConfig } = context;
// Find this plugin's configuration from handlerConfig
const wikiOperationPluginConfig = handlerConfig?.plugins?.find(p => p.pluginId === 'wikiOperation');
const wikiOperationParameter = wikiOperationPluginConfig?.wikiOperationParam as { toolResultDuration?: number } | undefined;
// Find this plugin's configuration import { AgentFrameworkConfig }
const wikiOperationToolConfig = agentFrameworkConfig?.plugins?.find((p: { toolId: string; [key: string]: unknown }) => p.toolId === 'wikiOperation');
const wikiOperationParameter = wikiOperationToolConfig?.wikiOperationParam as { toolResultDuration?: number } | undefined;
const toolResultDuration = wikiOperationParameter?.toolResultDuration || 1; // Default to 1 round
if (response.status !== 'done' || !response.content) {
@ -200,19 +200,19 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
logger.debug('Wiki operation tool call detected', {
toolId: toolMatch.toolId,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
});
// Set duration=1 for the AI message containing the tool call
// Find the most recent AI message (should be the one containing the tool call)
const aiMessages = handlerContext.agent.messages.filter(message => message.role === 'assistant');
const aiMessages = agentFrameworkContext.agent.messages.filter((message: AgentInstanceMessage) => message.role === 'assistant');
if (aiMessages.length > 0) {
const latestAiMessage = aiMessages[aiMessages.length - 1];
latestAiMessage.duration = toolResultDuration;
logger.debug('Set AI message duration for tool call', {
messageId: latestAiMessage.id,
duration: toolResultDuration,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
});
}
@ -220,7 +220,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
try {
logger.debug('Parsing wiki operation tool parameters', {
toolMatch: toolMatch.parameters,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
});
// Use parameters returned by matchToolCalling directly. Let zod schema validate.
@ -253,7 +253,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
workspaceName,
operation,
title,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
});
let result: string;
@ -293,7 +293,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
workspaceID,
operation,
title,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
});
// Format the tool result for display
@ -307,8 +307,8 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
logger.debug('Wiki operation setting yieldNextRoundTo=self', {
toolId: 'wiki-operation',
agentId: handlerContext.agent.id,
messageCount: handlerContext.agent.messages.length,
agentId: agentFrameworkContext.agent.id,
messageCount: agentFrameworkContext.agent.messages.length,
toolResultPreview: toolResultText.slice(0, 200),
});
@ -316,7 +316,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
const toolResultTime = new Date();
const toolResultMessage: AgentInstanceMessage = {
id: `tool-result-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
role: 'tool', // Tool result message
content: toolResultText,
modified: toolResultTime,
@ -331,7 +331,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
artificialOrder: Date.now() + 10, // Additional ordering hint
},
};
handlerContext.agent.messages.push(toolResultMessage);
agentFrameworkContext.agent.messages.push(toolResultMessage);
// Persist tool result immediately so DB ordering matches in-memory order
try {
@ -347,7 +347,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
// Signal that tool was executed AFTER adding and persisting the message
await hooks.toolExecuted.promise({
handlerContext,
agentFrameworkContext,
toolResult: {
success: true,
data: result,
@ -370,7 +370,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
} catch (error) {
logger.error('Wiki operation tool execution failed', {
error,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
toolParameters: toolMatch.parameters,
});
@ -389,7 +389,7 @@ Error: ${error instanceof Error ? error.message : String(error)}
const errorResultTime = new Date();
const errorResultMessage: AgentInstanceMessage = {
id: `tool-error-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
role: 'tool', // Tool error message
content: errorMessage,
modified: errorResultTime,
@ -402,11 +402,11 @@ Error: ${error instanceof Error ? error.message : String(error)}
isComplete: true, // Mark as complete to prevent messageManagementPlugin from overwriting content
},
};
handlerContext.agent.messages.push(errorResultMessage);
agentFrameworkContext.agent.messages.push(errorResultMessage);
// Signal that tool was executed (with error) AFTER adding the message
await hooks.toolExecuted.promise({
handlerContext,
agentFrameworkContext,
toolResult: {
success: false,
error: error instanceof Error ? error.message : String(error),

View file

@ -19,7 +19,7 @@ import { findPromptById } from '../promptConcat/promptConcat';
import type { AiAPIConfig } from '../promptConcat/promptConcatSchema';
import type { IPrompt } from '../promptConcat/promptConcatSchema';
import { schemaToToolContent } from '../utilities/schemaToToolContent';
import type { PromptConcatPlugin } from './types';
import type { PromptConcatTool } from './types';
/**
* Wiki Search Parameter Schema
@ -488,17 +488,17 @@ async function executeWikiUpdateEmbeddingsTool(
* Wiki Search plugin - Prompt processing
* Handles tool list injection for wiki search and update embeddings functionality
*/
export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
export const wikiSearchTool: PromptConcatTool = (hooks) => {
// First tapAsync: Tool list injection
hooks.processPrompts.tapAsync('wikiSearchPlugin-toolList', async (context, callback) => {
const { pluginConfig, prompts } = context;
hooks.processPrompts.tapAsync('wikiSearchTool-toolList', async (context, callback) => {
const { toolConfig, prompts } = context;
if (pluginConfig.pluginId !== 'wikiSearch' || !pluginConfig.wikiSearchParam) {
if (toolConfig.toolId !== 'wikiSearch' || !toolConfig.wikiSearchParam) {
callback();
return;
}
const wikiSearchParameter = pluginConfig.wikiSearchParam;
const wikiSearchParameter = toolConfig.wikiSearchParam;
try {
// Handle tool list injection if toolListPosition is configured
@ -508,7 +508,7 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
if (!toolListTarget) {
logger.warn('Tool list target prompt not found', {
targetId: toolListPosition.targetId,
pluginId: pluginConfig.id,
toolId: toolConfig.id,
});
callback();
return;
@ -544,7 +544,7 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
targetId: toolListPosition.targetId,
position: toolListPosition.position,
toolCount: 2, // wiki-search and wiki-update-embeddings
pluginId: pluginConfig.id,
toolId: toolConfig.id,
});
}
@ -552,20 +552,20 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
} catch (error) {
logger.error('Error in wiki search tool list injection', {
error,
pluginId: pluginConfig.id,
toolId: toolConfig.id,
});
callback();
}
});
// 2. Tool execution when AI response is complete
hooks.responseComplete.tapAsync('wikiSearchPlugin-handler', async (context, callback) => {
hooks.responseComplete.tapAsync('wikiSearchTool-handler', async (context, callback) => {
try {
const { handlerContext, response, handlerConfig } = context;
const { agentFrameworkContext, response, agentFrameworkConfig } = context;
// Find this plugin's configuration from handlerConfig
const wikiSearchPluginConfig = handlerConfig?.plugins?.find(p => p.pluginId === 'wikiSearch');
const wikiSearchParameter = wikiSearchPluginConfig?.wikiSearchParam as { toolResultDuration?: number } | undefined;
// Find this plugin's configuration import { AgentFrameworkConfig }
const wikiSearchToolConfig = agentFrameworkConfig?.plugins?.find((p: { toolId: string; [key: string]: unknown }) => p.toolId === 'wikiSearch');
const wikiSearchParameter = wikiSearchToolConfig?.wikiSearchParam as { toolResultDuration?: number } | undefined;
const toolResultDuration = wikiSearchParameter?.toolResultDuration || 1; // Default to 1 round
if (response.status !== 'done' || !response.content) {
@ -583,12 +583,12 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
logger.debug('Wiki tool call detected', {
toolId: toolMatch.toolId,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
});
// Set duration=1 for the AI message containing the tool call
// Find the most recent AI message (should be the one containing the tool call)
const aiMessages = handlerContext.agent.messages.filter(message => message.role === 'assistant');
const aiMessages = agentFrameworkContext.agent.messages.filter((message: AgentInstanceMessage) => message.role === 'assistant');
if (aiMessages.length > 0) {
const latestAiMessage = aiMessages[aiMessages.length - 1];
if (latestAiMessage.content === response.content) {
@ -614,7 +614,7 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
}
// Also update UI immediately
agentInstanceService.debounceUpdateMessage(latestAiMessage, handlerContext.agent.id, 0); // No delay
agentInstanceService.debounceUpdateMessage(latestAiMessage, agentFrameworkContext.agent.id, 0); // No delay
logger.debug('Set duration=1 for AI tool call message', {
messageId: latestAiMessage.id,
@ -626,10 +626,10 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
// Execute the appropriate tool
try {
// Check if cancelled before starting tool execution
if (handlerContext.isCancelled()) {
if (agentFrameworkContext.isCancelled()) {
logger.debug('Wiki tool cancelled before execution', {
toolId: toolMatch.toolId,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
});
callback();
return;
@ -644,9 +644,9 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
result = await executeWikiSearchTool(
validatedParameters,
{
agentId: handlerContext.agent.id,
messageId: handlerContext.agent.messages[handlerContext.agent.messages.length - 1]?.id,
config: handlerContext.agent.aiApiConfig as AiAPIConfig | undefined,
agentId: agentFrameworkContext.agent.id,
messageId: agentFrameworkContext.agent.messages[agentFrameworkContext.agent.messages.length - 1]?.id,
config: agentFrameworkContext.agent.aiApiConfig as AiAPIConfig | undefined,
},
);
} else {
@ -655,18 +655,18 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
result = await executeWikiUpdateEmbeddingsTool(
validatedParameters,
{
agentId: handlerContext.agent.id,
messageId: handlerContext.agent.messages[handlerContext.agent.messages.length - 1]?.id,
aiConfig: handlerContext.agent.aiApiConfig,
agentId: agentFrameworkContext.agent.id,
messageId: agentFrameworkContext.agent.messages[agentFrameworkContext.agent.messages.length - 1]?.id,
aiConfig: agentFrameworkContext.agent.aiApiConfig,
},
);
}
// Check if cancelled after tool execution
if (handlerContext.isCancelled()) {
if (agentFrameworkContext.isCancelled()) {
logger.debug('Wiki tool cancelled after execution', {
toolId: toolMatch.toolId,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
});
callback();
return;
@ -692,8 +692,8 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
logger.debug('Wiki search setting yieldNextRoundTo=self', {
toolId: 'wiki-search',
agentId: handlerContext.agent.id,
messageCount: handlerContext.agent.messages.length,
agentId: agentFrameworkContext.agent.id,
messageCount: agentFrameworkContext.agent.messages.length,
toolResultPreview: toolResultText.slice(0, 200),
});
@ -701,7 +701,7 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
const nowTool = new Date();
const toolResultMessage: AgentInstanceMessage = {
id: `tool-result-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
role: 'tool', // Tool result message
content: toolResultText,
created: nowTool,
@ -717,13 +717,13 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
artificialOrder: Date.now() + 10, // Additional ordering hint
},
};
handlerContext.agent.messages.push(toolResultMessage);
agentFrameworkContext.agent.messages.push(toolResultMessage);
// Do not persist immediately here. Let messageManagementPlugin handle persistence
// Signal that tool was executed AFTER adding and persisting the message
await hooks.toolExecuted.promise({
handlerContext,
agentFrameworkContext,
toolResult: {
success: true,
data: result.success ? result.data : result.error,
@ -765,7 +765,7 @@ Error: ${error instanceof Error ? error.message : String(error)}
const nowError = new Date();
const errorResultMessage: AgentInstanceMessage = {
id: `tool-error-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
agentId: handlerContext.agent.id,
agentId: agentFrameworkContext.agent.id,
role: 'tool', // Tool error message
content: errorMessage,
created: nowError,
@ -779,11 +779,11 @@ Error: ${error instanceof Error ? error.message : String(error)}
isComplete: true, // Mark as complete to prevent messageManagementPlugin from overwriting content
},
};
handlerContext.agent.messages.push(errorResultMessage);
agentFrameworkContext.agent.messages.push(errorResultMessage);
// Do not persist immediately; let messageManagementPlugin handle it during toolExecuted
await hooks.toolExecuted.promise({
handlerContext,
agentFrameworkContext,
toolResult: {
success: false,
error: error instanceof Error ? error.message : String(error),

View file

@ -45,23 +45,23 @@ import type { IWorkspaceService } from '@services/workspaces/interface';
import { isWikiWorkspace } from '@services/workspaces/interface';
import { findPromptById } from '../promptConcat/promptConcat';
import type { PromptConcatPlugin } from './types';
import type { PromptConcatTool } from './types';
/**
* Workspaces List plugin - Prompt processing
* Handles injection of available wiki workspaces list
*/
export const workspacesListPlugin: PromptConcatPlugin = (hooks) => {
export const workspacesListTool: PromptConcatTool = (hooks) => {
// Tool list injection
hooks.processPrompts.tapAsync('workspacesListPlugin-injection', async (context, callback) => {
const { pluginConfig, prompts } = context;
hooks.processPrompts.tapAsync('workspacesListTool-injection', async (context, callback) => {
const { toolConfig, prompts } = context;
if (pluginConfig.pluginId !== 'workspacesList' || !pluginConfig.workspacesListParam) {
if (toolConfig.toolId !== 'workspacesList' || !toolConfig.workspacesListParam) {
callback();
return;
}
const workspacesListParameter = pluginConfig.workspacesListParam;
const workspacesListParameter = toolConfig.workspacesListParam;
try {
// Handle workspaces list injection if targetId is configured
@ -70,7 +70,7 @@ export const workspacesListPlugin: PromptConcatPlugin = (hooks) => {
if (!target) {
logger.warn('Workspaces list target prompt not found', {
targetId: workspacesListParameter.targetId,
pluginId: pluginConfig.id,
toolId: toolConfig.id,
});
callback();
return;
@ -96,7 +96,7 @@ export const workspacesListPlugin: PromptConcatPlugin = (hooks) => {
}
const insertIndex = target.prompt.children.length;
target.prompt.children.splice(insertIndex, 0, {
id: `workspaces-list-${pluginConfig.id}`,
id: `workspaces-list-${toolConfig.id}`,
caption: 'Available Workspaces',
text: workspacesListContent,
});
@ -105,7 +105,7 @@ export const workspacesListPlugin: PromptConcatPlugin = (hooks) => {
target.prompt.children = [];
}
target.prompt.children.unshift({
id: `workspaces-list-${pluginConfig.id}`,
id: `workspaces-list-${toolConfig.id}`,
caption: 'Available Workspaces',
text: workspacesListContent,
});
@ -117,12 +117,12 @@ export const workspacesListPlugin: PromptConcatPlugin = (hooks) => {
logger.debug('Workspaces list injected successfully', {
targetId: workspacesListParameter.targetId,
position: workspacesListParameter.position,
pluginId: pluginConfig.id,
toolId: toolConfig.id,
workspaceCount: wikiWorkspaces.length,
});
} else {
logger.debug('No wiki workspaces found to inject', {
pluginId: pluginConfig.id,
toolId: toolConfig.id,
});
}
}
@ -131,7 +131,7 @@ export const workspacesListPlugin: PromptConcatPlugin = (hooks) => {
} catch (error) {
logger.error('Error in workspaces list injection', {
error,
pluginId: pluginConfig.id,
toolId: toolConfig.id,
});
callback();
}

View file

@ -14,8 +14,8 @@ export function createAgentInstanceData(agentDefinition: {
name: string;
avatarUrl?: string;
aiApiConfig?: Record<string, unknown>;
handlerConfig?: Record<string, unknown>;
handlerID?: string;
agentFrameworkConfig?: Record<string, unknown>;
agentFrameworkID?: string;
}): {
instanceData: Omit<AgentInstance, 'created' | 'modified'>;
instanceId: string;
@ -31,7 +31,7 @@ export function createAgentInstanceData(agentDefinition: {
};
// Extract necessary fields from agent definition
const { avatarUrl, aiApiConfig, handlerID } = agentDefinition;
const { avatarUrl, aiApiConfig, agentFrameworkID } = agentDefinition;
const instanceData = {
id: instanceId,
@ -40,9 +40,9 @@ export function createAgentInstanceData(agentDefinition: {
status: initialStatus,
avatarUrl,
aiApiConfig,
// Don't copy handlerConfig to instance - it should fallback to definition
handlerConfig: undefined,
handlerID,
// Don't copy agentFrameworkConfig to instance - it should fallback to definition
agentFrameworkConfig: undefined,
agentFrameworkID,
messages: [],
closed: false,
};
@ -119,6 +119,6 @@ export const AGENT_INSTANCE_FIELDS = [
'modified',
'avatarUrl',
'aiApiConfig',
'handlerConfig',
'agentFrameworkConfig',
'closed',
] as const;

View file

@ -29,11 +29,11 @@ export class AgentDefinitionEntity implements Partial<AgentDefinition> {
/** Agent handler function ID, nullable indicates using default handler */
@Column({ nullable: true })
handlerID?: string;
agentFrameworkID?: string;
/** Agent handler configuration parameters, stored as JSON */
@Column({ type: 'simple-json', nullable: true })
handlerConfig?: Record<string, unknown>;
agentFrameworkConfig?: Record<string, unknown>;
/** Agent's AI API configuration, can override global default config */
@Column({ type: 'simple-json', nullable: true })
@ -88,7 +88,7 @@ export class AgentInstanceEntity implements Partial<AgentInstance> {
/** Agent handler configuration parameters, inherited from AgentDefinition */
@Column({ type: 'simple-json', nullable: true })
handlerConfig?: Record<string, unknown>;
agentFrameworkConfig?: Record<string, unknown>;
@Column({ default: false })
closed: boolean = false;

View file

@ -1,5 +1,5 @@
import type { AgentDefinition } from '@services/agentDefinition/interface';
import defaultAgents from '@services/agentInstance/buildInAgentHandlers/defaultAgents.json';
import defaultAgents from '@services/agentInstance/agentFrameworks/taskAgents.json';
import { container } from '@services/container';
import type { IDatabaseService } from '@services/database/interface';
import { AgentDefinitionEntity } from '@services/database/schema/agent';

View file

@ -105,7 +105,7 @@ export async function registerMenu(): Promise<void> {
dir: activeWorkspace.wikiFolderLocation,
commitOnly: false,
commitMessage: i18n.t('LOG.CommitBackupMessage'),
remoteUrl: activeWorkspace.gitUrl,
remoteUrl: activeWorkspace.gitUrl ?? undefined,
userInfo,
});
}
@ -127,7 +127,7 @@ export async function registerMenu(): Promise<void> {
dir: activeWorkspace.wikiFolderLocation,
commitOnly: false,
// Don't provide commitMessage to trigger AI generation
remoteUrl: activeWorkspace.gitUrl,
remoteUrl: activeWorkspace.gitUrl ?? undefined,
userInfo,
});
}
@ -151,7 +151,7 @@ export async function registerMenu(): Promise<void> {
dir: activeWorkspace.wikiFolderLocation,
commitOnly: false,
commitMessage: i18n.t('LOG.CommitBackupMessage'),
remoteUrl: activeWorkspace.gitUrl,
remoteUrl: activeWorkspace.gitUrl ?? undefined,
userInfo,
});
}

View file

@ -0,0 +1,102 @@
import { AgentFrameworkConfig } from '@services/agentInstance/promptConcat/promptConcatSchema';
import { useCallback, useEffect, useState } from 'react';
interface useAgentFrameworkConfigManagementProps {
agentDefId?: string;
agentId?: string;
}
interface UseAgentFrameworkConfigManagementResult {
loading: boolean;
config: AgentFrameworkConfig | undefined;
schema?: Record<string, unknown>;
handleConfigChange: (newConfig: AgentFrameworkConfig) => Promise<void>;
}
export const useAgentFrameworkConfigManagement = ({ agentDefId, agentId }: useAgentFrameworkConfigManagementProps = {}): UseAgentFrameworkConfigManagementResult => {
const [loading, setLoading] = useState(true);
const [config, setConfig] = useState<AgentFrameworkConfig | undefined>(undefined);
const [schema, setSchema] = useState<Record<string, unknown> | undefined>(undefined);
useEffect(() => {
const fetchConfig = async () => {
try {
setLoading(true);
let finalConfig: AgentFrameworkConfig | undefined;
let agentFrameworkID: string | undefined;
if (agentId) {
const agentInstance = await window.service.agentInstance.getAgent(agentId);
let agentDefinition: Awaited<ReturnType<typeof window.service.agentDefinition.getAgentDef>> | undefined;
if (agentInstance?.agentDefId) {
agentDefinition = await window.service.agentDefinition.getAgentDef(agentInstance.agentDefId);
}
// Use instance config if available, otherwise fallback to definition config
if (agentInstance?.agentFrameworkConfig && Object.keys(agentInstance.agentFrameworkConfig).length > 0) {
finalConfig = agentInstance.agentFrameworkConfig as AgentFrameworkConfig;
} else if (agentDefinition?.agentFrameworkConfig) {
finalConfig = agentDefinition.agentFrameworkConfig as AgentFrameworkConfig;
}
// Use agentFrameworkID from instance, fallback to definition
agentFrameworkID = agentInstance?.agentFrameworkID || agentDefinition?.agentFrameworkID;
} else if (agentDefId) {
const agentDefinition = await window.service.agentDefinition.getAgentDef(agentDefId);
if (agentDefinition?.agentFrameworkConfig) {
finalConfig = agentDefinition.agentFrameworkConfig as AgentFrameworkConfig;
}
agentFrameworkID = agentDefinition?.agentFrameworkID;
}
if (agentFrameworkID) {
try {
const frameworkSchema = await window.service.agentInstance.getFrameworkConfigSchema(agentFrameworkID);
setSchema(frameworkSchema);
} catch (error) {
void window.service.native.log('error', 'Failed to load framework schema', { function: 'useAgentFrameworkConfigManagement.fetchConfig', error });
}
}
setConfig(finalConfig);
setLoading(false);
} catch (error) {
void window.service.native.log('error', 'Failed to load framework configuration', { function: 'useAgentFrameworkConfigManagement.fetchConfig', error });
setLoading(false);
}
};
void fetchConfig();
}, [agentDefId, agentId]);
const handleConfigChange = useCallback(async (newConfig: AgentFrameworkConfig) => {
try {
setConfig(newConfig);
if (agentId) {
await window.service.agentInstance.updateAgent(agentId, {
agentFrameworkConfig: newConfig,
});
} else if (agentDefId) {
const agentDefinition = await window.service.agentDefinition.getAgentDef(agentDefId);
if (agentDefinition) {
await window.service.agentDefinition.updateAgentDef({
...agentDefinition,
agentFrameworkConfig: newConfig,
});
}
} else {
void window.service.native.log('error', 'No agent ID or definition ID provided for updating handler config', {
function: 'useAgentFrameworkConfigManagement.handleConfigChange',
});
}
} catch (error) {
void window.service.native.log('error', 'Failed to update framework configuration', { function: 'useAgentFrameworkConfigManagement.handleConfigChange', error });
}
}, [agentId, agentDefId]);
return {
loading,
config,
schema,
handleConfigChange,
};
};

View file

@ -1,100 +0,0 @@
import { HandlerConfig } from '@services/agentInstance/promptConcat/promptConcatSchema';
import { useCallback, useEffect, useState } from 'react';
interface UseHandlerConfigManagementProps {
agentDefId?: string;
agentId?: string;
}
interface UseHandlerConfigManagementResult {
loading: boolean;
config: HandlerConfig | undefined;
schema?: Record<string, unknown>;
handleConfigChange: (newConfig: HandlerConfig) => Promise<void>;
}
export const useHandlerConfigManagement = ({ agentDefId, agentId }: UseHandlerConfigManagementProps = {}): UseHandlerConfigManagementResult => {
const [loading, setLoading] = useState(true);
const [config, setConfig] = useState<HandlerConfig | undefined>(undefined);
const [schema, setSchema] = useState<Record<string, unknown> | undefined>(undefined);
useEffect(() => {
const fetchConfig = async () => {
try {
setLoading(true);
let finalConfig: HandlerConfig | undefined;
let handlerID: string | undefined;
if (agentId) {
const agentInstance = await window.service.agentInstance.getAgent(agentId);
let agentDefinition: Awaited<ReturnType<typeof window.service.agentDefinition.getAgentDef>> | undefined;
if (agentInstance?.agentDefId) {
agentDefinition = await window.service.agentDefinition.getAgentDef(agentInstance.agentDefId);
}
// Use instance config if available, otherwise fallback to definition config
if (agentInstance?.handlerConfig && Object.keys(agentInstance.handlerConfig).length > 0) {
finalConfig = agentInstance.handlerConfig as HandlerConfig;
} else if (agentDefinition?.handlerConfig) {
finalConfig = agentDefinition.handlerConfig as HandlerConfig;
}
// Use handlerID from instance, fallback to definition
handlerID = agentInstance?.handlerID || agentDefinition?.handlerID;
} else if (agentDefId) {
const agentDefinition = await window.service.agentDefinition.getAgentDef(agentDefId);
if (agentDefinition?.handlerConfig) {
finalConfig = agentDefinition.handlerConfig as HandlerConfig;
}
handlerID = agentDefinition?.handlerID;
}
if (handlerID) {
try {
const handlerSchema = await window.service.agentInstance.getHandlerConfigSchema(handlerID);
setSchema(handlerSchema);
} catch (error) {
void window.service.native.log('error', 'Failed to load handler schema', { function: 'useHandlerConfigManagement.fetchConfig', error });
}
}
setConfig(finalConfig);
setLoading(false);
} catch (error) {
void window.service.native.log('error', 'Failed to load handler configuration', { function: 'useHandlerConfigManagement.fetchConfig', error });
setLoading(false);
}
};
void fetchConfig();
}, [agentDefId, agentId]);
const handleConfigChange = useCallback(async (newConfig: HandlerConfig) => {
try {
setConfig(newConfig);
if (agentId) {
await window.service.agentInstance.updateAgent(agentId, {
handlerConfig: newConfig,
});
} else if (agentDefId) {
const agentDefinition = await window.service.agentDefinition.getAgentDef(agentDefId);
if (agentDefinition) {
await window.service.agentDefinition.updateAgentDef({
...agentDefinition,
handlerConfig: newConfig,
});
}
} else {
void window.service.native.log('error', 'No agent ID or definition ID provided for updating handler config', { function: 'useHandlerConfigManagement.handleConfigChange' });
}
} catch (error) {
void window.service.native.log('error', 'Failed to update handler configuration', { function: 'useHandlerConfigManagement.handleConfigChange', error });
}
}, [agentId, agentDefId]);
return {
loading,
config,
schema,
handleConfigChange,
};
};