@radaros/core 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,65 @@
1
1
  import { createRequire } from "node:module";
2
2
  import type { ToolCall } from "../models/types.js";
3
3
  import type { RunContext } from "../agent/run-context.js";
4
- import type { ToolDef, ToolCallResult } from "./types.js";
4
+ import type { ToolDef, ToolCallResult, ToolResult } from "./types.js";
5
5
 
6
6
  const _require = createRequire(import.meta.url);
7
7
 
8
+ interface CacheEntry {
9
+ result: string | ToolResult;
10
+ expiresAt: number;
11
+ }
12
+
8
13
  export class ToolExecutor {
9
14
  private tools: Map<string, ToolDef>;
10
15
  private concurrency: number;
16
+ private cache = new Map<string, CacheEntry>();
17
+ private cachedDefs: Array<{
18
+ name: string;
19
+ description: string;
20
+ parameters: Record<string, unknown>;
21
+ }> | null = null;
11
22
 
12
23
  constructor(tools: ToolDef[], concurrency: number = 5) {
13
24
  this.tools = new Map(tools.map((t) => [t.name, t]));
14
25
  this.concurrency = concurrency;
26
+ this.cachedDefs = this.buildToolDefinitions();
27
+ }
28
+
29
+ clearCache(): void {
30
+ this.cache.clear();
31
+ }
32
+
33
+ private getCacheKey(toolName: string, args: Record<string, unknown>): string {
34
+ const sortedArgs = JSON.stringify(args, Object.keys(args).sort());
35
+ return `${toolName}:${sortedArgs}`;
36
+ }
37
+
38
+ private getCached(toolName: string, args: Record<string, unknown>): (string | ToolResult) | undefined {
39
+ const tool = this.tools.get(toolName);
40
+ if (!tool?.cache) return undefined;
41
+
42
+ const key = this.getCacheKey(toolName, args);
43
+ const entry = this.cache.get(key);
44
+ if (!entry) return undefined;
45
+
46
+ if (Date.now() > entry.expiresAt) {
47
+ this.cache.delete(key);
48
+ return undefined;
49
+ }
50
+
51
+ return entry.result;
52
+ }
53
+
54
+ private setCache(toolName: string, args: Record<string, unknown>, result: string | ToolResult): void {
55
+ const tool = this.tools.get(toolName);
56
+ if (!tool?.cache) return;
57
+
58
+ const key = this.getCacheKey(toolName, args);
59
+ this.cache.set(key, {
60
+ result,
61
+ expiresAt: Date.now() + tool.cache.ttl,
62
+ });
15
63
  }
16
64
 
17
65
  async executeAll(
@@ -66,6 +114,24 @@ export class ToolExecutor {
66
114
  args: toolCall.arguments,
67
115
  });
68
116
 
117
+ const cachedResult = this.getCached(toolCall.name, toolCall.arguments);
118
+ if (cachedResult !== undefined) {
119
+ const resultContent =
120
+ typeof cachedResult === "string" ? cachedResult : cachedResult.content;
121
+
122
+ ctx.eventBus.emit("tool.result", {
123
+ runId: ctx.runId,
124
+ toolName: toolCall.name,
125
+ result: `[cached] ${resultContent}`,
126
+ });
127
+
128
+ return {
129
+ toolCallId: toolCall.id,
130
+ toolName: toolCall.name,
131
+ result: cachedResult,
132
+ };
133
+ }
134
+
69
135
  const parsed = tool.parameters.safeParse(toolCall.arguments);
70
136
  if (!parsed.success) {
71
137
  const errMsg = `Invalid arguments: ${parsed.error.message}`;
@@ -89,6 +155,8 @@ export class ToolExecutor {
89
155
  const resultContent =
90
156
  typeof rawResult === "string" ? rawResult : rawResult.content;
91
157
 
158
+ this.setCache(toolCall.name, toolCall.arguments, rawResult);
159
+
92
160
  ctx.eventBus.emit("tool.result", {
93
161
  runId: ctx.runId,
94
162
  toolName: toolCall.name,
@@ -106,6 +174,16 @@ export class ToolExecutor {
106
174
  name: string;
107
175
  description: string;
108
176
  parameters: Record<string, unknown>;
177
+ }> {
178
+ if (this.cachedDefs) return this.cachedDefs;
179
+ this.cachedDefs = this.buildToolDefinitions();
180
+ return this.cachedDefs;
181
+ }
182
+
183
+ private buildToolDefinitions(): Array<{
184
+ name: string;
185
+ description: string;
186
+ parameters: Record<string, unknown>;
109
187
  }> {
110
188
  const { zodToJsonSchema } = _require("zod-to-json-schema");
111
189
  const defs: Array<{
@@ -123,13 +201,17 @@ export class ToolExecutor {
123
201
  });
124
202
  } else {
125
203
  const jsonSchema = zodToJsonSchema(tool.parameters, {
126
- target: "openApi3",
127
- });
204
+ target: "jsonSchema7",
205
+ $refStrategy: "none",
206
+ }) as Record<string, unknown>;
207
+
208
+ delete jsonSchema["$schema"];
209
+ delete jsonSchema["additionalProperties"];
128
210
 
129
211
  defs.push({
130
212
  name: tool.name,
131
213
  description: tool.description,
132
- parameters: jsonSchema as Record<string, unknown>,
214
+ parameters: jsonSchema,
133
215
  });
134
216
  }
135
217
  }
@@ -12,6 +12,11 @@ export interface ToolResult {
12
12
  artifacts?: Artifact[];
13
13
  }
14
14
 
15
+ export interface ToolCacheConfig {
16
+ /** Time-to-live in milliseconds. Cached results expire after this duration. */
17
+ ttl: number;
18
+ }
19
+
15
20
  export interface ToolDef {
16
21
  name: string;
17
22
  description: string;
@@ -19,6 +24,8 @@ export interface ToolDef {
19
24
  execute: (args: Record<string, unknown>, ctx: RunContext) => Promise<string | ToolResult>;
20
25
  /** Raw JSON Schema to send to the LLM, bypassing Zod-to-JSON conversion (used by MCP tools). */
21
26
  rawJsonSchema?: Record<string, unknown>;
27
+ /** Enable result caching for this tool. */
28
+ cache?: ToolCacheConfig;
22
29
  }
23
30
 
24
31
  export interface ToolCallResult {
@@ -0,0 +1,56 @@
1
+ export interface RetryConfig {
2
+ maxRetries: number;
3
+ initialDelayMs: number;
4
+ maxDelayMs: number;
5
+ retryableErrors?: (error: unknown) => boolean;
6
+ }
7
+
8
+ const DEFAULT_CONFIG: RetryConfig = {
9
+ maxRetries: 3,
10
+ initialDelayMs: 500,
11
+ maxDelayMs: 10_000,
12
+ retryableErrors: isRetryableError,
13
+ };
14
+
15
+ function isRetryableError(error: unknown): boolean {
16
+ if (error && typeof error === "object") {
17
+ const status = (error as any).status ?? (error as any).statusCode;
18
+ if (status === 429 || (status >= 500 && status < 600)) return true;
19
+
20
+ const code = (error as any).code;
21
+ if (code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ENOTFOUND") return true;
22
+
23
+ const msg = (error as any).message;
24
+ if (typeof msg === "string" && /rate.limit|too.many.requests|overloaded/i.test(msg)) return true;
25
+ }
26
+ return false;
27
+ }
28
+
29
+ function sleep(ms: number): Promise<void> {
30
+ return new Promise((resolve) => setTimeout(resolve, ms));
31
+ }
32
+
33
+ export async function withRetry<T>(
34
+ fn: () => Promise<T>,
35
+ config?: Partial<RetryConfig>
36
+ ): Promise<T> {
37
+ const cfg = { ...DEFAULT_CONFIG, ...config };
38
+ let lastError: unknown;
39
+
40
+ for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
41
+ try {
42
+ return await fn();
43
+ } catch (error) {
44
+ lastError = error;
45
+ if (attempt >= cfg.maxRetries || !cfg.retryableErrors!(error)) throw error;
46
+
47
+ const delay = Math.min(
48
+ cfg.initialDelayMs * Math.pow(2, attempt) + Math.random() * 200,
49
+ cfg.maxDelayMs
50
+ );
51
+ await sleep(delay);
52
+ }
53
+ }
54
+
55
+ throw lastError;
56
+ }