From c1a5a2bdf57fd6887e830626e00090ac3ad1dba5 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Fri, 27 Feb 2026 18:15:35 +0800 Subject: [PATCH 1/6] refactor: simplify retry --- packages/ema/src/agent.ts | 2 +- packages/ema/src/config.ts | 20 ++--- packages/ema/src/llm/google_client.ts | 10 +-- packages/ema/src/llm/openai_client.ts | 10 +-- packages/ema/src/{ => llm}/retry.ts | 103 ++++++++++---------------- 5 files changed, 62 insertions(+), 83 deletions(-) rename packages/ema/src/{ => llm}/retry.ts (55%) diff --git a/packages/ema/src/agent.ts b/packages/ema/src/agent.ts index 6efc1e0..8c17c35 100644 --- a/packages/ema/src/agent.ts +++ b/packages/ema/src/agent.ts @@ -2,7 +2,7 @@ import { EventEmitter } from "node:events"; import { type LLMClient } from "./llm"; import { AgentConfig } from "./config"; import { Logger } from "./logger"; -import { RetryExhaustedError, isAbortError } from "./retry"; +import { RetryExhaustedError, isAbortError } from "./llm/retry"; import type { LLMResponse, Message, Content, FunctionResponse } from "./schema"; import type { Tool, ToolResult, ToolContext } from "./tools/base"; import type { EmaReply } from "./tools/ema_reply_tool"; diff --git a/packages/ema/src/config.ts b/packages/ema/src/config.ts index 0970268..b2ceef3 100644 --- a/packages/ema/src/config.ts +++ b/packages/ema/src/config.ts @@ -15,10 +15,10 @@ import { fileURLToPath } from "node:url"; import yaml from "js-yaml"; -import { RetryConfig } from "./retry"; +import { RetryConfig } from "./llm/retry"; import { type Tool, baseTools } from "./tools"; import { skillsPrompt } from "./skills"; -export { RetryConfig } from "./retry"; +export { RetryConfig } from "./llm/retry"; /** * MongoDB configuration. @@ -59,7 +59,7 @@ export class MongoConfig { * The MongoDB database name. */ public readonly db_name: string = "ema", - ) {} + ) { } } /** @@ -78,7 +78,7 @@ export class SystemConfig { * If it is empty, no proxy will be used. */ public https_proxy: string = "", - ) {} + ) { } } /** @@ -128,7 +128,7 @@ export class OpenAIApiConfig implements LLMApiConfig { * If environment variable OPENAI_API_BASE is set, it will be used first. */ public base_url: string = "https://api.openai.com/v1", - ) {} + ) { } } /** @@ -164,7 +164,7 @@ export class GoogleApiConfig implements LLMApiConfig { * If environment variable GEMINI_API_BASE is set, it will be used first. */ public base_url: string = "https://generativelanguage.googleapis.com", - ) {} + ) { } } /** @@ -224,7 +224,7 @@ export class LLMConfig { * Retry configuration for the LLM provider. */ public readonly retry: RetryConfig = new RetryConfig(), - ) {} + ) { } } /** @@ -248,7 +248,7 @@ export class AgentConfig { * The token limit for the agent. */ public readonly tokenLimit: number = 80000, - ) {} + ) { } } /** @@ -282,7 +282,7 @@ export class ToolsConfig { * The MCP config path. */ public readonly mcp_config_path: string = "mcp.json", - ) {} + ) { } } /** @@ -324,7 +324,7 @@ export class Config { * System configuration */ public readonly system: SystemConfig, - ) {} + ) { } /** * Loads configuration from the default search path. diff --git a/packages/ema/src/llm/google_client.ts b/packages/ema/src/llm/google_client.ts index 775bcd0..7a0ccf3 100644 --- a/packages/ema/src/llm/google_client.ts +++ b/packages/ema/src/llm/google_client.ts @@ -8,7 +8,7 @@ import { } from "../schema"; import type { Content, LLMResponse, Message, SchemaAdapter } from "../schema"; import type { Tool } from "../tools"; -import { wrapWithRetry } from "../retry"; +import { wrapWithRetry } from "./retry"; import { FetchWithProxy } from "./proxy"; import { GenerateContentResponse as GenAIResponse, @@ -294,10 +294,10 @@ export class GoogleClient extends LLMClientBase implements SchemaAdapter { const executor = this.retryConfig.enabled ? wrapWithRetry( - this.makeApiRequest.bind(this), - this.retryConfig, - this.retryCallback, - ) + this.makeApiRequest.bind(this), + this.retryConfig, + this.retryCallback, + ) : this.makeApiRequest.bind(this); const response = await executor( diff --git a/packages/ema/src/llm/openai_client.ts b/packages/ema/src/llm/openai_client.ts index b2af7e7..8a25ec4 100644 --- a/packages/ema/src/llm/openai_client.ts +++ b/packages/ema/src/llm/openai_client.ts @@ -18,7 +18,7 @@ import { } from "../schema"; import type { Content, LLMResponse, Message, ModelMessage } from "../schema"; import type { Tool } from "../tools/base"; -import { wrapWithRetry } from "../retry"; +import { wrapWithRetry } from "./retry"; import type { LLMApiConfig, RetryConfig } from "../config"; import { FetchWithProxy } from "./proxy"; @@ -266,10 +266,10 @@ export class OpenAIClient extends LLMClientBase implements SchemaAdapter { const executor = this.retryConfig.enabled ? wrapWithRetry( - this.makeApiRequest.bind(this), - this.retryConfig, - this.retryCallback, - ) + this.makeApiRequest.bind(this), + this.retryConfig, + this.retryCallback, + ) : this.makeApiRequest.bind(this); const response = await executor( diff --git a/packages/ema/src/retry.ts b/packages/ema/src/llm/retry.ts similarity index 55% rename from packages/ema/src/retry.ts rename to packages/ema/src/llm/retry.ts index 97a409e..7c3a760 100644 --- a/packages/ema/src/retry.ts +++ b/packages/ema/src/llm/retry.ts @@ -36,7 +36,7 @@ export class RetryConfig { * Retryable exception types */ // public readonly retryable_exceptions: Array = [Error], - ) {} + ) { } } /** @@ -80,9 +80,11 @@ export function isAbortError(error: unknown): boolean { } /** - * Async function retry decorator. + * Wrap a standalone async function with retry logic (non-decorator usage). + * Useful when you want a callable instead of applying a class method decorator. */ -export function asyncRetry( +export function wrapWithRetry Promise>( + originalMethod: T, /** * Retry configuration */ @@ -91,66 +93,43 @@ export function asyncRetry( * Callback function on retry, receives exception and current attempt number */ onRetry?: (exception: Error, attempt: number) => void, -): ( - target: any, - propertyKey: string, - descriptor: PropertyDescriptor, -) => PropertyDescriptor { - return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { - const originalMethod = descriptor.value; - descriptor.value = async function (...args: any[]) { - let lastException: Error | undefined; - for (let attempt = 0; attempt <= config.max_retries; attempt++) { - try { - return await originalMethod.apply(this, args); - } catch (exception) { - lastException = exception as Error; - if (isAbortError(lastException)) { - throw lastException; - } - if (attempt >= config.max_retries) { - console.error( - `Function ${propertyKey} retry failed, reached maximum retry count ${config.max_retries}`, - ); - throw new RetryExhaustedError(lastException, attempt + 1); - } - const delay = calculateDelay( - attempt, - config.initial_delay, - config.exponential_base, - config.max_delay, - ); - console.warn( - `Function ${propertyKey} call ${attempt + 1} failed: ${lastException.message}, retrying attempt ${attempt + 2} after ${delay.toFixed(2)} seconds`, +): T { + return async function (...args: any[]) { + let lastException: Error | undefined; + for (let attempt = 0; attempt <= config.max_retries; attempt++) { + try { + return await originalMethod(args); + } catch (exception) { + lastException = exception as Error; + if (isAbortError(lastException)) { + throw lastException; + } + if (attempt >= config.max_retries) { + console.error( + `Function retry failed, reached maximum retry count ${config.max_retries}`, ); - // Call callback function - if (onRetry) { - onRetry(lastException, attempt + 1); - } - // Wait before retry - await new Promise((resolve) => setTimeout(resolve, delay * 1000)); + throw new RetryExhaustedError(lastException, attempt + 1); } + const delay = calculateDelay( + attempt, + config.initial_delay, + config.exponential_base, + config.max_delay, + ); + console.warn( + `Function call ${attempt + 1} failed: ${lastException.message}, retrying attempt ${attempt + 2} after ${delay.toFixed(2)} seconds`, + ); + // Call callback function + if (onRetry) { + onRetry(lastException, attempt + 1); + } + // Wait before retry + await new Promise((resolve) => setTimeout(resolve, delay * 1000)); } - if (lastException) { - throw lastException; - } - throw new Error("Unknown error"); - }; - return descriptor; - }; -} - -/** - * Wrap a standalone async function with retry logic (non-decorator usage). - * Useful when you want a callable instead of applying a class method decorator. - */ -export function wrapWithRetry Promise>( - fn: T, - config: RetryConfig = new RetryConfig(), - onRetry?: (exception: Error, attempt: number) => void, -): T { - const decorator = asyncRetry(config, onRetry); - const descriptor: PropertyDescriptor = { value: fn }; - const wrappedDescriptor = decorator({}, "wrapped", descriptor) ?? descriptor; - return (wrappedDescriptor.value ?? fn) as T; + } + if (lastException) { + throw lastException; + } + throw new Error("Unknown error"); + } as T; } From a434a019523a8928f5279b587c3d13301936e240 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Fri, 27 Feb 2026 18:18:02 +0800 Subject: [PATCH 2/6] dev: format code --- packages/ema/src/config.ts | 16 ++++++++-------- packages/ema/src/llm/google_client.ts | 8 ++++---- packages/ema/src/llm/openai_client.ts | 8 ++++---- packages/ema/src/llm/retry.ts | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/ema/src/config.ts b/packages/ema/src/config.ts index b2ceef3..5da04eb 100644 --- a/packages/ema/src/config.ts +++ b/packages/ema/src/config.ts @@ -59,7 +59,7 @@ export class MongoConfig { * The MongoDB database name. */ public readonly db_name: string = "ema", - ) { } + ) {} } /** @@ -78,7 +78,7 @@ export class SystemConfig { * If it is empty, no proxy will be used. */ public https_proxy: string = "", - ) { } + ) {} } /** @@ -128,7 +128,7 @@ export class OpenAIApiConfig implements LLMApiConfig { * If environment variable OPENAI_API_BASE is set, it will be used first. */ public base_url: string = "https://api.openai.com/v1", - ) { } + ) {} } /** @@ -164,7 +164,7 @@ export class GoogleApiConfig implements LLMApiConfig { * If environment variable GEMINI_API_BASE is set, it will be used first. */ public base_url: string = "https://generativelanguage.googleapis.com", - ) { } + ) {} } /** @@ -224,7 +224,7 @@ export class LLMConfig { * Retry configuration for the LLM provider. */ public readonly retry: RetryConfig = new RetryConfig(), - ) { } + ) {} } /** @@ -248,7 +248,7 @@ export class AgentConfig { * The token limit for the agent. */ public readonly tokenLimit: number = 80000, - ) { } + ) {} } /** @@ -282,7 +282,7 @@ export class ToolsConfig { * The MCP config path. */ public readonly mcp_config_path: string = "mcp.json", - ) { } + ) {} } /** @@ -324,7 +324,7 @@ export class Config { * System configuration */ public readonly system: SystemConfig, - ) { } + ) {} /** * Loads configuration from the default search path. diff --git a/packages/ema/src/llm/google_client.ts b/packages/ema/src/llm/google_client.ts index 7a0ccf3..cceba31 100644 --- a/packages/ema/src/llm/google_client.ts +++ b/packages/ema/src/llm/google_client.ts @@ -294,10 +294,10 @@ export class GoogleClient extends LLMClientBase implements SchemaAdapter { const executor = this.retryConfig.enabled ? wrapWithRetry( - this.makeApiRequest.bind(this), - this.retryConfig, - this.retryCallback, - ) + this.makeApiRequest.bind(this), + this.retryConfig, + this.retryCallback, + ) : this.makeApiRequest.bind(this); const response = await executor( diff --git a/packages/ema/src/llm/openai_client.ts b/packages/ema/src/llm/openai_client.ts index 8a25ec4..9adcdfd 100644 --- a/packages/ema/src/llm/openai_client.ts +++ b/packages/ema/src/llm/openai_client.ts @@ -266,10 +266,10 @@ export class OpenAIClient extends LLMClientBase implements SchemaAdapter { const executor = this.retryConfig.enabled ? wrapWithRetry( - this.makeApiRequest.bind(this), - this.retryConfig, - this.retryCallback, - ) + this.makeApiRequest.bind(this), + this.retryConfig, + this.retryCallback, + ) : this.makeApiRequest.bind(this); const response = await executor( diff --git a/packages/ema/src/llm/retry.ts b/packages/ema/src/llm/retry.ts index 7c3a760..3b51500 100644 --- a/packages/ema/src/llm/retry.ts +++ b/packages/ema/src/llm/retry.ts @@ -36,7 +36,7 @@ export class RetryConfig { * Retryable exception types */ // public readonly retryable_exceptions: Array = [Error], - ) { } + ) {} } /** From c05ac4b7c8566aafbf72d547576a3c5c8aa85419 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Fri, 27 Feb 2026 18:27:31 +0800 Subject: [PATCH 3/6] fix: test --- packages/ema/src/llm/retry.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ema/src/llm/retry.ts b/packages/ema/src/llm/retry.ts index 3b51500..a078c07 100644 --- a/packages/ema/src/llm/retry.ts +++ b/packages/ema/src/llm/retry.ts @@ -36,7 +36,7 @@ export class RetryConfig { * Retryable exception types */ // public readonly retryable_exceptions: Array = [Error], - ) {} + ) { } } /** @@ -98,7 +98,7 @@ export function wrapWithRetry Promise>( let lastException: Error | undefined; for (let attempt = 0; attempt <= config.max_retries; attempt++) { try { - return await originalMethod(args); + return await originalMethod(...args); } catch (exception) { lastException = exception as Error; if (isAbortError(lastException)) { From 360c9591482c555b09aac02bafe343dc0b591735 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Fri, 27 Feb 2026 18:28:49 +0800 Subject: [PATCH 4/6] fix: fmt --- packages/ema/src/llm/retry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ema/src/llm/retry.ts b/packages/ema/src/llm/retry.ts index a078c07..912a813 100644 --- a/packages/ema/src/llm/retry.ts +++ b/packages/ema/src/llm/retry.ts @@ -36,7 +36,7 @@ export class RetryConfig { * Retryable exception types */ // public readonly retryable_exceptions: Array = [Error], - ) { } + ) {} } /** From bf01f637ee20147f5aee698f86875f98eb04eaee Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Fri, 27 Feb 2026 18:34:21 +0800 Subject: [PATCH 5/6] fix: logic error --- packages/ema/src/llm/retry.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ema/src/llm/retry.ts b/packages/ema/src/llm/retry.ts index 912a813..cab1971 100644 --- a/packages/ema/src/llm/retry.ts +++ b/packages/ema/src/llm/retry.ts @@ -36,7 +36,7 @@ export class RetryConfig { * Retryable exception types */ // public readonly retryable_exceptions: Array = [Error], - ) {} + ) { } } /** @@ -94,6 +94,9 @@ export function wrapWithRetry Promise>( */ onRetry?: (exception: Error, attempt: number) => void, ): T { + if (config.max_retries <= 0) { + throw new Error("Max retries must be greater than 0"); + } return async function (...args: any[]) { let lastException: Error | undefined; for (let attempt = 0; attempt <= config.max_retries; attempt++) { From fbcc1642d48831a2cb709a68a4cc801c86c83d65 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Fri, 27 Feb 2026 18:35:22 +0800 Subject: [PATCH 6/6] fix: fmt --- packages/ema/src/llm/retry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ema/src/llm/retry.ts b/packages/ema/src/llm/retry.ts index cab1971..994a1b0 100644 --- a/packages/ema/src/llm/retry.ts +++ b/packages/ema/src/llm/retry.ts @@ -36,7 +36,7 @@ export class RetryConfig { * Retryable exception types */ // public readonly retryable_exceptions: Array = [Error], - ) { } + ) {} } /**