task loop

This commit is contained in:
锦恢 2025-06-20 21:06:01 +08:00
parent be641197b5
commit 4a8385d7ad
12 changed files with 416 additions and 362 deletions

View File

@ -39,22 +39,22 @@ export class MessageBridge {
switch (platform) { switch (platform) {
case 'vscode': case 'vscode':
this.setupVsCodeListener(); this.setupVsCodeListener();
pinkLog('当前模式: vscode'); pinkLog('current platform: vscode');
break; break;
case 'electron': case 'electron':
this.setupElectronListener(); this.setupElectronListener();
pinkLog('当前模式: electron'); pinkLog('current platform: electron');
break; break;
case 'nodejs': case 'nodejs':
this.setupNodejsListener(); this.setupNodejsListener();
pinkLog('当前模式: nodejs'); pinkLog('current platform: nodejs');
break; break;
case 'web': case 'web':
this.setupWebSocket(); this.setupWebSocket();
pinkLog('当前模式: web'); pinkLog('current platform: web');
break; break;
} }
} }

View File

@ -65,7 +65,7 @@ export class TaskLoop {
}; };
constructor( constructor(
private readonly taskOptions: TaskLoopOptions = { private taskOptions: TaskLoopOptions = {
maxEpochs: 50, maxEpochs: 50,
maxJsonParseRetry: 3, maxJsonParseRetry: 3,
adapter: undefined, adapter: undefined,
@ -99,6 +99,25 @@ export class TaskLoop {
mcpClientAdapter.addConnectRefreshListener(); mcpClientAdapter.addConnectRefreshListener();
} }
public async waitConnection() {
await this.nodejsStatus.connectionFut;
}
public setTaskLoopOptions(taskOptions: TaskLoopOptions) {
const {
maxEpochs = 50,
maxJsonParseRetry = 3,
verbose = 1,
} = taskOptions;
this.taskOptions = {
maxEpochs,
maxJsonParseRetry,
verbose,
...this.taskOptions
};
}
/** /**
* @description streaming content * @description streaming content
* @param chunk * @param chunk
@ -784,9 +803,11 @@ export class TaskLoop {
} }
} }
public async getPrompt(promptId: string, args: Record<string, any>) { public async getPrompt(promptId: string, args?: Record<string, any>) {
const prompt = await mcpClientAdapter.readPromptTemplate(promptId, args); const prompt = await mcpClientAdapter.readPromptTemplate(promptId, args);
return prompt; // transform prompt to string
const promptString = prompt.messages.map(m => m.content.text).join('\n');
return promptString;
} }
public async getResource(resourceUri: string) { public async getResource(resourceUri: string) {

View File

@ -677,7 +677,7 @@ class McpClientAdapter {
return msg; return msg;
} }
public async readPromptTemplate(promptId: string, args: Record<string, any>) { public async readPromptTemplate(promptId: string, args?: Record<string, any>) {
// TODO: 如果遇到不同服务器的同名 tool请拓展解决方案 // TODO: 如果遇到不同服务器的同名 tool请拓展解决方案
// 目前只找到第一个匹配 toolName 的工具进行调用 // 目前只找到第一个匹配 toolName 的工具进行调用
let clientId = this.clients[0].clientId; let clientId = this.clients[0].clientId;
@ -691,6 +691,8 @@ class McpClientAdapter {
} }
} }
console.log(args);
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest<PromptsGetResponse>('prompts/get', { clientId, promptId, args }); const { code, msg } = await bridge.commandRequest<PromptsGetResponse>('prompts/get', { clientId, promptId, args });
return msg; return msg;

View File

@ -189,6 +189,17 @@ interface ChatSetting {
export class TaskLoop { export class TaskLoop {
constructor(taskOptions?: TaskLoopOptions); constructor(taskOptions?: TaskLoopOptions);
/**
* @description wait for connection
*/
waitConnection(): Promise<void>;
/**
* @description Set the task loop options
* @param taskOptions
*/
setTaskLoopOptions(taskOptions: TaskLoopOptions): void;
/** /**
* @description make chat data * @description make chat data
* @param tabStorage * @param tabStorage
@ -294,7 +305,7 @@ export class TaskLoop {
/** /**
* @description Get prompt template from mcp server * @description Get prompt template from mcp server
*/ */
getPrompt(promptId: string, args: Record<string, any>): Promise<string>; getPrompt(promptId: string, args?: Record<string, any>): Promise<string>;
/** /**
* @description Get resource template from mcp server * @description Get resource template from mcp server

View File

@ -1,8 +1,4 @@
import * as fs from 'fs';
import { WebSocket } from 'ws'; import { WebSocket } from 'ws';
import { EventEmitter } from 'events';
import { routeMessage } from '../common/router.js';
import { ConnectionType } from '../mcp/client.dto.js'; import { ConnectionType } from '../mcp/client.dto.js';
// WebSocket 消息格式 // WebSocket 消息格式
@ -80,318 +76,3 @@ export class VSCodeWebViewLike {
}; };
} }
} }
export class TaskLoopAdapter {
public emitter: EventEmitter;
private messageHandlers: Set<MessageHandler>;
private connectionOptions: IConnectionArgs[] = [];
constructor(option?: any) {
this.emitter = new EventEmitter(option);
this.messageHandlers = new Set();
this.emitter.on('message/renderer', (message: WebSocketMessage) => {
this.messageHandlers.forEach((handler) => handler(message));
});
// 默认需要将监听的消息导入到 routeMessage 中
this.onDidReceiveMessage((message) => {
const { command, data } = message;
switch (command) {
case 'nodejs/launch-signature':
this.postMessage({
command: 'nodejs/launch-signature',
data: {
code: 200,
msg: this.connectionOptions
}
})
break;
case 'nodejs/update-connection-signature':
// sdk 模式下不需要自动保存连接参数
break;
default:
routeMessage(command, data, this);
break;
}
});
}
/**
* @description
* @param message - command args
*/
public postMessage(message: WebSocketMessage): void {
this.emitter.emit('message/service', message);
}
/**
* @description
* @param callback -
* @returns {{ dispose: () => void }} -
*/
public onDidReceiveMessage(callback: MessageHandler): { dispose: () => void } {
this.messageHandlers.add(callback);
return {
dispose: () => this.messageHandlers.delete(callback),
};
}
/**
* @description mcp
* @param mcpOption
*/
public addMcp(mcpOption: IConnectionArgs) {
// 0.1.4 新版本开始,此处修改为懒加载连接
// 实际的连接移交给前端 mcpAdapter 中进行统一的调度
// 调度步骤如下:
// getLaunchSignature 先获取访问签名,访问签名通过当前函数 push 到 class 中
this.connectionOptions.push(mcpOption);
}
}
interface StdioMCPConfig {
command: string;
args: string[];
env?: {
[key: string]: string;
};
description?: string;
prompts?: string[];
resources?: string[];
}
interface HttpMCPConfig {
url: string;
type?: string;
env?: {
[key: string]: string;
};
description?: string;
prompts?: string[];
resources?: string[];
}
export interface OmAgentConfiguration {
version: string;
mcpServers: {
[key: string]: StdioMCPConfig | HttpMCPConfig;
};
defaultLLM: {
baseURL: string;
apiToken: string;
model: string;
}
}
export interface DefaultLLM {
baseURL: string;
apiToken?: string;
model: string;
}
import {
MessageState,
type TaskLoopOptions,
type ChatMessage,
type ChatSetting,
type TaskLoop,
type TextMessage
} from '../../task-loop.js';
export function UserMessage(content: string): TextMessage {
return {
role: 'user',
content,
extraInfo: {
created: Date.now(),
state: MessageState.None,
serverName: '',
enableXmlWrapper: false
}
}
}
export function AssistantMessage(content: string): TextMessage {
return {
role: 'assistant',
content,
extraInfo: {
created: Date.now(),
state: MessageState.None,
serverName: '',
enableXmlWrapper: false
}
}
}
export class OmAgent {
private _adapter: TaskLoopAdapter;
private _loop?: TaskLoop;
private _defaultLLM?: DefaultLLM;
constructor() {
this._adapter = new TaskLoopAdapter();
}
/**
* @description Load MCP configuration from file.
* Supports multiple MCP backends and a default LLM model configuration.
*
* @example
* Example configuration:
* {
* "version": "1.0.0",
* "mcpServers": {
* "openmemory": {
* "command": "npx",
* "args": ["-y", "openmemory"],
* "env": {
* "OPENMEMORY_API_KEY": "YOUR_API_KEY",
* "CLIENT_NAME": "openmemory"
* },
* "description": "A MCP for long-term memory support"
* }
* },
* "defaultLLM": {
* "baseURL": "https://api.openmemory.ai",
* "apiToken": "YOUR_API_KEY",
* "model": "deepseek-chat"
* }
* }
*
* @param configPath - Path to the configuration file
*/
public loadMcpConfig(configPath: string) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as OmAgentConfiguration;
const { mcpServers, defaultLLM } = config;
// set default llm
this.setDefaultLLM(defaultLLM);
for (const key in mcpServers) {
const mcpConfig = mcpServers[key];
if ('command' in mcpConfig) {
const commandString = (
mcpConfig.command + ' ' + mcpConfig.args.join(' ')
).trim();
this._adapter.addMcp({
commandString,
connectionType: 'STDIO',
env: mcpConfig.env,
description: mcpConfig.description,
});
} else {
const connectionType: ConnectionType = mcpConfig.type === 'http' ? 'STREAMABLE_HTTP' : 'SSE';
this._adapter.addMcp({
url: mcpConfig.url,
env: mcpConfig.env,
connectionType,
description: mcpConfig.description,
});
}
}
}
/**
* @description Add MCP server
*/
public addMcpServer(connectionArgs: IConnectionArgs) {
this._adapter.addMcp(connectionArgs);
}
private async getLoop(loopOption?: TaskLoopOptions) {
if (this._loop) {
return this._loop;
}
const {
verbose = 1,
maxEpochs = 50,
maxJsonParseRetry = 3,
} = loopOption || {}
const adapter = this._adapter;
const { TaskLoop } = await import('../../task-loop.js');
this._loop = new TaskLoop({ adapter, verbose, maxEpochs, maxJsonParseRetry });
return this._loop;
}
public setDefaultLLM(option: DefaultLLM) {
this._defaultLLM = option;
}
public async getPrompt(promptId: string, args: Record<string, any>) {
const loop = await this.getLoop();
const prompt = await loop.getPrompt(promptId, args);
return prompt;
}
/**
* @description Asynchronous invoking agent by string or messages
* @param messages Chat message or string
* @param settings Chat setting and task loop options
* @returns
*/
public async ainvoke(
{ messages, settings }: { messages: ChatMessage[] | string; settings?: ChatSetting & Partial<TaskLoopOptions>; }
) {
if (messages.length === 0) {
throw new Error('messages is empty');
}
// detach taskloop option from settings and set default value
const {
maxEpochs = 50,
maxJsonParseRetry = 3,
verbose = 1
} = settings || {};
const loop = await this.getLoop({ maxEpochs, maxJsonParseRetry, verbose });
const storage = await loop.createStorage(settings);
// set input message
// user can invoke [UserMessage("CONTENT")] to make messages quickly
// or use string directly
let userMessage: string;
if (typeof messages === 'string') {
userMessage = messages;
} else {
const lastMessageContent = messages.at(-1)?.content;
if (typeof lastMessageContent === 'string') {
userMessage = lastMessageContent;
} else {
throw new Error('last message content is undefined');
}
}
// select correct llm config
// user can set llm config via omagent.setDefaultLLM()
// or write "defaultLLM" in mcpconfig.json to specify
if (this._defaultLLM) {
loop.setLlmConfig({
baseUrl: this._defaultLLM.baseURL,
userToken: this._defaultLLM.apiToken,
userModel: this._defaultLLM.model,
});
} else {
// throw error to user and give the suggestion
throw new Error('default LLM is not set, please set it via omagent.setDefaultLLM() or write "defaultLLM" in mcpconfig.json');
}
await loop.start(storage, userMessage);
// get response from last message in message list
const lastMessage = storage.messages.at(-1)?.content;
return lastMessage;
}
}

327
service/src/hook/sdk.ts Normal file
View File

@ -0,0 +1,327 @@
import { EventEmitter } from 'events';
import { routeMessage } from '../common/router.js';
import * as fs from 'fs';
export class TaskLoopAdapter {
public emitter: EventEmitter;
private messageHandlers: Set<MessageHandler>;
private connectionOptions: IConnectionArgs[] = [];
constructor(option?: any) {
this.emitter = new EventEmitter(option);
this.messageHandlers = new Set();
this.emitter.on('message/renderer', (message: WebSocketMessage) => {
this.messageHandlers.forEach((handler) => handler(message));
});
// 默认需要将监听的消息导入到 routeMessage 中
this.onDidReceiveMessage((message) => {
const { command, data } = message;
switch (command) {
case 'nodejs/launch-signature':
this.postMessage({
command: 'nodejs/launch-signature',
data: {
code: 200,
msg: this.connectionOptions
}
})
break;
case 'nodejs/update-connection-signature':
// sdk 模式下不需要自动保存连接参数
break;
default:
routeMessage(command, data, this);
break;
}
});
}
/**
* @description
* @param message - command args
*/
public postMessage(message: WebSocketMessage): void {
this.emitter.emit('message/service', message);
}
/**
* @description
* @param callback -
* @returns {{ dispose: () => void }} -
*/
public onDidReceiveMessage(callback: MessageHandler): { dispose: () => void } {
this.messageHandlers.add(callback);
return {
dispose: () => this.messageHandlers.delete(callback),
};
}
/**
* @description mcp
* @param mcpOption
*/
public addMcp(mcpOption: IConnectionArgs) {
// 0.1.4 新版本开始,此处修改为懒加载连接
// 实际的连接移交给前端 mcpAdapter 中进行统一的调度
// 调度步骤如下:
// getLaunchSignature 先获取访问签名,访问签名通过当前函数 push 到 class 中
this.connectionOptions.push(mcpOption);
}
}
interface StdioMCPConfig {
command: string;
args: string[];
env?: {
[key: string]: string;
};
description?: string;
prompts?: string[];
resources?: string[];
}
interface HttpMCPConfig {
url: string;
type?: string;
env?: {
[key: string]: string;
};
description?: string;
prompts?: string[];
resources?: string[];
}
export interface OmAgentConfiguration {
version: string;
mcpServers: {
[key: string]: StdioMCPConfig | HttpMCPConfig;
};
defaultLLM: {
baseURL: string;
apiToken: string;
model: string;
}
}
export interface DefaultLLM {
baseURL: string;
apiToken?: string;
model: string;
}
import {
MessageState,
type TaskLoopOptions,
type ChatMessage,
type ChatSetting,
type TaskLoop,
type TextMessage
} from '../../task-loop.js';
import { IConnectionArgs, MessageHandler, WebSocketMessage } from './adapter.js';
import { ConnectionType } from 'src/mcp/client.dto.js';
export function UserMessage(content: string): TextMessage {
return {
role: 'user',
content,
extraInfo: {
created: Date.now(),
state: MessageState.None,
serverName: '',
enableXmlWrapper: false
}
}
}
export function AssistantMessage(content: string): TextMessage {
return {
role: 'assistant',
content,
extraInfo: {
created: Date.now(),
state: MessageState.None,
serverName: '',
enableXmlWrapper: false
}
}
}
export class OmAgent {
private _adapter: TaskLoopAdapter;
private _loop?: TaskLoop;
private _defaultLLM?: DefaultLLM;
constructor() {
this._adapter = new TaskLoopAdapter();
}
/**
* @description Load MCP configuration from file.
* Supports multiple MCP backends and a default LLM model configuration.
*
* @example
* Example configuration:
* {
* "version": "1.0.0",
* "mcpServers": {
* "openmemory": {
* "command": "npx",
* "args": ["-y", "openmemory"],
* "env": {
* "OPENMEMORY_API_KEY": "YOUR_API_KEY",
* "CLIENT_NAME": "openmemory"
* },
* "description": "A MCP for long-term memory support"
* }
* },
* "defaultLLM": {
* "baseURL": "https://api.openmemory.ai",
* "apiToken": "YOUR_API_KEY",
* "model": "deepseek-chat"
* }
* }
*
* @param configPath - Path to the configuration file
*/
public loadMcpConfig(configPath: string) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as OmAgentConfiguration;
const { mcpServers, defaultLLM } = config;
// set default llm
this.setDefaultLLM(defaultLLM);
for (const key in mcpServers) {
const mcpConfig = mcpServers[key];
if ('command' in mcpConfig) {
const commandString = (
mcpConfig.command + ' ' + mcpConfig.args.join(' ')
).trim();
this._adapter.addMcp({
commandString,
connectionType: 'STDIO',
env: mcpConfig.env,
description: mcpConfig.description,
});
} else {
const connectionType: ConnectionType = mcpConfig.type === 'http' ? 'STREAMABLE_HTTP' : 'SSE';
this._adapter.addMcp({
url: mcpConfig.url,
env: mcpConfig.env,
connectionType,
description: mcpConfig.description,
});
}
}
}
/**
* @description Add MCP server
*/
public addMcpServer(connectionArgs: IConnectionArgs) {
this._adapter.addMcp(connectionArgs);
}
private async getLoop(loopOption?: TaskLoopOptions) {
if (this._loop) {
if (loopOption) {
this._loop.setTaskLoopOptions(loopOption);
}
return this._loop;
}
const {
verbose = 1,
maxEpochs = 50,
maxJsonParseRetry = 3,
} = loopOption || {}
const adapter = this._adapter;
const { TaskLoop } = await import('../../task-loop.js');
this._loop = new TaskLoop({ adapter, verbose, maxEpochs, maxJsonParseRetry });
await this._loop.waitConnection();
return this._loop;
}
public setDefaultLLM(option: DefaultLLM) {
this._defaultLLM = option;
}
public async getPrompt(promptId: string, args: Record<string, any>) {
const loop = await this.getLoop();
const prompt = await loop.getPrompt(promptId, args);
return prompt;
}
/**
* @description Asynchronous invoking agent by string or messages
* @param messages Chat message or string
* @param settings Chat setting and task loop options
* @returns
*/
public async ainvoke(
{ messages, settings }: { messages: ChatMessage[] | string; settings?: ChatSetting & Partial<TaskLoopOptions>; }
) {
if (messages.length === 0) {
throw new Error('messages is empty');
}
// detach taskloop option from settings and set default value
const {
maxEpochs = 50,
maxJsonParseRetry = 3,
verbose = 1
} = settings || {};
const loop = await this.getLoop({ maxEpochs, maxJsonParseRetry, verbose });
const storage = await loop.createStorage(settings);
// set input message
// user can invoke [UserMessage("CONTENT")] to make messages quickly
// or use string directly
let userMessage: string;
if (typeof messages === 'string') {
userMessage = messages;
} else {
const lastMessageContent = messages.at(-1)?.content;
if (typeof lastMessageContent === 'string') {
userMessage = lastMessageContent;
} else {
throw new Error('last message content is undefined');
}
}
// select correct llm config
// user can set llm config via omagent.setDefaultLLM()
// or write "defaultLLM" in mcpconfig.json to specify
if (this._defaultLLM) {
loop.setLlmConfig({
baseUrl: this._defaultLLM.baseURL,
userToken: this._defaultLLM.apiToken,
userModel: this._defaultLLM.model,
});
} else {
// throw error to user and give the suggestion
throw new Error('default LLM is not set, please set it via omagent.setDefaultLLM() or write "defaultLLM" in mcpconfig.json');
}
await loop.start(storage, userMessage);
// get response from last message in message list
const lastMessage = storage.messages.at(-1)?.content;
return lastMessage;
}
}

View File

@ -1,4 +1,4 @@
export { routeMessage } from './common/router.js'; export { routeMessage } from './common/router.js';
export { VSCodeWebViewLike, TaskLoopAdapter, OmAgent, OmAgentConfiguration, UserMessage, AssistantMessage } from './hook/adapter.js'; export { VSCodeWebViewLike } from './hook/adapter.js';
export { setVscodeWorkspace, setRunningCWD } from './hook/setting.js'; export { setVscodeWorkspace, setRunningCWD } from './hook/setting.js';
export { clientMap } from './mcp/connect.service.js'; export { clientMap } from './mcp/connect.service.js';

View File

@ -268,24 +268,24 @@ export async function connectService(
): Promise<RestfulResponse> { ): Promise<RestfulResponse> {
try { try {
// 使用cli-table3创建美观的表格 // 使用cli-table3创建美观的表格
const table = new Table({ // const table = new Table({
head: ['Property', 'Value'], // head: ['Property', 'Value'],
colWidths: [20, 60], // colWidths: [20, 60],
style: { // style: {
head: ['green'], // head: ['green'],
border: ['grey'] // border: ['grey']
} // }
}); // });
table.push( // table.push(
['Connection Type', option.connectionType], // ['Connection Type', option.connectionType],
['Command', option.command || 'N/A'], // ['Command', option.command || 'N/A'],
['Arguments', option.args?.join(' ') || 'N/A'], // ['Arguments', option.args?.join(' ') || 'N/A'],
['Working Directory', option.cwd || 'N/A'], // ['Working Directory', option.cwd || 'N/A'],
['URL', option.url || 'N/A'] // ['URL', option.url || 'N/A']
); // );
console.log(table.toString()); // console.log(table.toString());
// 预处理字符串 // 预处理字符串
await preprocessCommand(option, webview); await preprocessCommand(option, webview);

1
service/src/sdk.ts Normal file
View File

@ -0,0 +1 @@
export { TaskLoopAdapter, OmAgent, OmAgentConfiguration, UserMessage, AssistantMessage } from './hook/sdk.js';

View File

@ -189,6 +189,17 @@ interface ChatSetting {
export class TaskLoop { export class TaskLoop {
constructor(taskOptions?: TaskLoopOptions); constructor(taskOptions?: TaskLoopOptions);
/**
* @description wait for connection
*/
waitConnection(): Promise<void>;
/**
* @description Set the task loop options
* @param taskOptions
*/
setTaskLoopOptions(taskOptions: TaskLoopOptions): void;
/** /**
* @description make chat data * @description make chat data
* @param tabStorage * @param tabStorage
@ -294,7 +305,7 @@ export class TaskLoop {
/** /**
* @description Get prompt template from mcp server * @description Get prompt template from mcp server
*/ */
getPrompt(promptId: string, args: Record<string, any>): Promise<string>; getPrompt(promptId: string, args?: Record<string, any>): Promise<string>;
/** /**
* @description Get resource template from mcp server * @description Get resource template from mcp server

View File

@ -10,7 +10,7 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"outDir": "./dist", "outDir": "./dist",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": false,
"experimentalDecorators": true, "experimentalDecorators": true,
}, },
"paths": { "paths": {