lynkr 8.0.1 → 9.0.2

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.
Files changed (55) hide show
  1. package/README.md +238 -315
  2. package/bin/cli.js +16 -3
  3. package/index.js +7 -3
  4. package/install.sh +3 -3
  5. package/lynkr-skill.tar.gz +0 -0
  6. package/native/Cargo.toml +26 -0
  7. package/native/index.js +29 -0
  8. package/native/lynkr-native.node +0 -0
  9. package/native/src/lib.rs +321 -0
  10. package/package.json +8 -6
  11. package/src/api/files-multipart.js +30 -0
  12. package/src/api/files-router.js +81 -0
  13. package/src/api/openai-router.js +379 -308
  14. package/src/api/providers-handler.js +171 -3
  15. package/src/api/router.js +109 -5
  16. package/src/cache/prompt.js +13 -0
  17. package/src/clients/circuit-breaker.js +10 -247
  18. package/src/clients/codex-process.js +342 -0
  19. package/src/clients/codex-utils.js +143 -0
  20. package/src/clients/databricks.js +243 -76
  21. package/src/clients/ollama-utils.js +21 -17
  22. package/src/clients/openai-format.js +20 -6
  23. package/src/clients/openrouter-utils.js +42 -37
  24. package/src/clients/prompt-cache-injection.js +140 -0
  25. package/src/clients/provider-capabilities.js +41 -0
  26. package/src/clients/resilience.js +540 -0
  27. package/src/clients/responses-format.js +8 -7
  28. package/src/clients/retry.js +22 -167
  29. package/src/clients/standard-tools.js +1 -1
  30. package/src/clients/xml-tool-extractor.js +307 -0
  31. package/src/cluster.js +82 -0
  32. package/src/config/index.js +66 -0
  33. package/src/context/compression.js +42 -9
  34. package/src/context/distill.js +507 -0
  35. package/src/context/tool-result-compressor.js +563 -0
  36. package/src/memory/extractor.js +22 -0
  37. package/src/orchestrator/index.js +147 -205
  38. package/src/routing/complexity-analyzer.js +258 -5
  39. package/src/routing/index.js +15 -34
  40. package/src/routing/latency-tracker.js +148 -0
  41. package/src/routing/model-tiers.js +2 -0
  42. package/src/routing/quality-scorer.js +113 -0
  43. package/src/routing/telemetry.js +502 -0
  44. package/src/server.js +23 -0
  45. package/src/stores/file-store.js +69 -0
  46. package/src/stores/response-store.js +25 -0
  47. package/src/tools/code-graph.js +538 -0
  48. package/src/tools/code-mode.js +304 -0
  49. package/src/tools/index.js +1 -1
  50. package/src/tools/lazy-loader.js +11 -0
  51. package/src/tools/mcp-remote.js +7 -0
  52. package/src/tools/smart-selection.js +11 -0
  53. package/src/tools/web.js +1 -1
  54. package/src/utils/payload.js +206 -0
  55. package/src/utils/perf-timer.js +80 -0
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Code Mode — Meta-Tools for MCP Token Optimization
3
+ *
4
+ * Replaces 100+ individual MCP tool definitions with 4 meta-tools,
5
+ * reducing tool-catalog token overhead from ~17,500 to ~700 tokens.
6
+ *
7
+ * Inspired by Bifrost's Code Mode. Instead of sending every MCP tool
8
+ * schema in every request, the LLM discovers tools lazily:
9
+ * 1. mcp_list_tools → discover available tools (compact)
10
+ * 2. mcp_tool_info → load full schema for one tool
11
+ * 3. mcp_tool_docs → get usage examples
12
+ * 4. mcp_execute → execute a tool by name
13
+ *
14
+ * Activation: CODE_MODE_ENABLED=true
15
+ *
16
+ * @module tools/code-mode
17
+ */
18
+
19
+ const { registerTool } = require('.');
20
+ const { listServers, ensureClient } = require('../mcp');
21
+ const config = require('../config');
22
+ const logger = require('../logger');
23
+
24
+ // ── Tool List Cache ─────────────────────────────────────────────────
25
+
26
+ let toolListCache = null;
27
+ let toolListCacheTs = 0;
28
+
29
+ function getCacheTtl() {
30
+ return config.mcp?.codeMode?.toolListCacheTtl || 60_000;
31
+ }
32
+
33
+ /**
34
+ * Fetch tool lists from all MCP servers, with caching.
35
+ * @param {string} [filterServerId] - Optional: only fetch from this server
36
+ * @param {boolean} [forceRefresh] - Bypass cache
37
+ * @returns {Promise<Object>} { serverId: [{ name, description }] }
38
+ */
39
+ async function fetchToolList(filterServerId, forceRefresh = false) {
40
+ const now = Date.now();
41
+ if (!forceRefresh && toolListCache && (now - toolListCacheTs < getCacheTtl())) {
42
+ if (filterServerId) {
43
+ return { [filterServerId]: toolListCache[filterServerId] || [] };
44
+ }
45
+ return toolListCache;
46
+ }
47
+
48
+ const servers = listServers();
49
+ const result = {};
50
+
51
+ await Promise.all(
52
+ servers.map(async (server) => {
53
+ if (filterServerId && server.id !== filterServerId) return;
54
+ try {
55
+ const client = await ensureClient(server.id);
56
+ if (!client) {
57
+ result[server.id] = { error: 'Server not available' };
58
+ return;
59
+ }
60
+ const response = await client.request('tools/list', {});
61
+ const tools = Array.isArray(response?.tools) ? response.tools : [];
62
+ result[server.id] = tools.map(t => ({
63
+ name: t.name ?? t.method ?? 'unknown',
64
+ description: (t.description || '').substring(0, 100),
65
+ }));
66
+ } catch (err) {
67
+ result[server.id] = { error: err.message };
68
+ }
69
+ })
70
+ );
71
+
72
+ // Update cache if we fetched all servers
73
+ if (!filterServerId) {
74
+ toolListCache = result;
75
+ toolListCacheTs = now;
76
+ }
77
+
78
+ return filterServerId ? { [filterServerId]: result[filterServerId] || [] } : result;
79
+ }
80
+
81
+ /**
82
+ * Fetch full tool schema for a specific tool on a specific server.
83
+ * @param {string} serverId
84
+ * @param {string} toolName
85
+ * @returns {Promise<Object|null>}
86
+ */
87
+ async function fetchToolSchema(serverId, toolName) {
88
+ const client = await ensureClient(serverId);
89
+ if (!client) return null;
90
+
91
+ const response = await client.request('tools/list', {});
92
+ const tools = Array.isArray(response?.tools) ? response.tools : [];
93
+ return tools.find(t => (t.name ?? t.method) === toolName) || null;
94
+ }
95
+
96
+ /**
97
+ * Generate a usage example from a tool's input schema.
98
+ * @param {Object} tool - Tool definition with inputSchema
99
+ * @returns {string} Example JSON
100
+ */
101
+ function generateExample(tool) {
102
+ const schema = tool.inputSchema || tool.input_schema || {};
103
+ const props = schema.properties || {};
104
+ const example = {};
105
+
106
+ for (const [key, def] of Object.entries(props)) {
107
+ if (def.type === 'string') example[key] = def.example || `<${key}>`;
108
+ else if (def.type === 'number' || def.type === 'integer') example[key] = def.example || 0;
109
+ else if (def.type === 'boolean') example[key] = def.example ?? true;
110
+ else if (def.type === 'array') example[key] = [];
111
+ else if (def.type === 'object') example[key] = {};
112
+ else example[key] = null;
113
+ }
114
+
115
+ return JSON.stringify(example, null, 2);
116
+ }
117
+
118
+ // ── Meta-Tool Registration ──────────────────────────────────────────
119
+
120
+ function registerCodeModeTools() {
121
+ // 1. mcp_list_tools — discover available tools
122
+ registerTool(
123
+ 'mcp_list_tools',
124
+ async ({ args = {} }) => {
125
+ const serverId = args.server_id || null;
126
+ const forceRefresh = args.force_refresh === true;
127
+ const result = await fetchToolList(serverId, forceRefresh);
128
+
129
+ // Add summary stats
130
+ let totalTools = 0;
131
+ for (const tools of Object.values(result)) {
132
+ if (Array.isArray(tools)) totalTools += tools.length;
133
+ }
134
+
135
+ return {
136
+ ok: true,
137
+ status: 200,
138
+ content: JSON.stringify({ total_tools: totalTools, servers: result }, null, 2),
139
+ };
140
+ },
141
+ {
142
+ category: 'code-mode',
143
+ description: 'List all available MCP tools across all servers. Returns tool names and brief descriptions. Use this first to discover what tools are available.',
144
+ input_schema: {
145
+ type: 'object',
146
+ properties: {
147
+ server_id: { type: 'string', description: 'Optional: filter to a specific MCP server ID' },
148
+ force_refresh: { type: 'boolean', description: 'Bypass cache and refresh tool list' },
149
+ },
150
+ },
151
+ }
152
+ );
153
+
154
+ // 2. mcp_tool_info — load full schema for one tool
155
+ registerTool(
156
+ 'mcp_tool_info',
157
+ async ({ args = {} }) => {
158
+ const serverId = args.server_id;
159
+ const toolName = args.tool_name;
160
+
161
+ if (!serverId || !toolName) {
162
+ throw new Error('mcp_tool_info requires server_id and tool_name');
163
+ }
164
+
165
+ const tool = await fetchToolSchema(serverId, toolName);
166
+ if (!tool) {
167
+ throw new Error(`Tool "${toolName}" not found on server "${serverId}"`);
168
+ }
169
+
170
+ return {
171
+ ok: true,
172
+ status: 200,
173
+ content: JSON.stringify({
174
+ server: serverId,
175
+ name: tool.name ?? tool.method,
176
+ description: tool.description || '',
177
+ inputSchema: tool.inputSchema || tool.input_schema || {},
178
+ }, null, 2),
179
+ };
180
+ },
181
+ {
182
+ category: 'code-mode',
183
+ description: 'Get the full schema and detailed description for a specific MCP tool. Use after mcp_list_tools to get the exact parameters needed before calling mcp_execute.',
184
+ input_schema: {
185
+ type: 'object',
186
+ properties: {
187
+ server_id: { type: 'string', description: 'MCP server ID' },
188
+ tool_name: { type: 'string', description: 'Tool name from mcp_list_tools' },
189
+ },
190
+ required: ['server_id', 'tool_name'],
191
+ },
192
+ }
193
+ );
194
+
195
+ // 3. mcp_tool_docs — usage examples
196
+ registerTool(
197
+ 'mcp_tool_docs',
198
+ async ({ args = {} }) => {
199
+ const serverId = args.server_id;
200
+ const toolName = args.tool_name;
201
+
202
+ if (!serverId || !toolName) {
203
+ throw new Error('mcp_tool_docs requires server_id and tool_name');
204
+ }
205
+
206
+ const tool = await fetchToolSchema(serverId, toolName);
207
+ if (!tool) {
208
+ throw new Error(`Tool "${toolName}" not found on server "${serverId}"`);
209
+ }
210
+
211
+ const schema = tool.inputSchema || tool.input_schema || {};
212
+ const params = Object.entries(schema.properties || {}).map(([name, def]) => ({
213
+ name,
214
+ type: def.type || 'any',
215
+ required: (schema.required || []).includes(name),
216
+ description: def.description || '',
217
+ }));
218
+
219
+ return {
220
+ ok: true,
221
+ status: 200,
222
+ content: JSON.stringify({
223
+ server: serverId,
224
+ tool: tool.name ?? tool.method,
225
+ description: tool.description || '',
226
+ parameters: params,
227
+ example_arguments: generateExample(tool),
228
+ usage: `Use mcp_execute with server_id="${serverId}", tool_name="${toolName}", and arguments matching the schema above.`,
229
+ }, null, 2),
230
+ };
231
+ },
232
+ {
233
+ category: 'code-mode',
234
+ description: 'Get usage documentation, parameter details, and example arguments for an MCP tool.',
235
+ input_schema: {
236
+ type: 'object',
237
+ properties: {
238
+ server_id: { type: 'string', description: 'MCP server ID' },
239
+ tool_name: { type: 'string', description: 'Tool name' },
240
+ },
241
+ required: ['server_id', 'tool_name'],
242
+ },
243
+ }
244
+ );
245
+
246
+ // 4. mcp_execute — execute a tool by name
247
+ registerTool(
248
+ 'mcp_execute',
249
+ async ({ args = {} }) => {
250
+ const serverId = args.server_id;
251
+ const toolName = args.tool_name;
252
+ const toolArgs = args.arguments ?? {};
253
+
254
+ if (!serverId || !toolName) {
255
+ throw new Error('mcp_execute requires server_id and tool_name');
256
+ }
257
+
258
+ const client = await ensureClient(serverId.trim());
259
+ if (!client) {
260
+ throw new Error(`MCP server "${serverId}" is not available.`);
261
+ }
262
+
263
+ const result = await client.request(toolName.trim(), toolArgs);
264
+
265
+ return {
266
+ ok: true,
267
+ status: 200,
268
+ content: JSON.stringify({
269
+ server: serverId,
270
+ tool: toolName,
271
+ result,
272
+ }, null, 2),
273
+ metadata: { server: serverId, tool: toolName },
274
+ };
275
+ },
276
+ {
277
+ category: 'code-mode',
278
+ description: 'Execute an MCP tool by name with JSON arguments. First use mcp_list_tools to discover tools, then mcp_tool_info to get the schema, then this tool to execute.',
279
+ input_schema: {
280
+ type: 'object',
281
+ properties: {
282
+ server_id: { type: 'string', description: 'MCP server ID' },
283
+ tool_name: { type: 'string', description: 'Tool method name' },
284
+ arguments: {
285
+ type: 'object',
286
+ description: 'JSON arguments matching the tool input schema',
287
+ additionalProperties: true,
288
+ },
289
+ },
290
+ required: ['server_id', 'tool_name'],
291
+ },
292
+ }
293
+ );
294
+
295
+ logger.info('[code-mode] Registered 4 meta-tools: mcp_list_tools, mcp_tool_info, mcp_tool_docs, mcp_execute');
296
+ }
297
+
298
+ module.exports = {
299
+ registerCodeModeTools,
300
+ // Exported for testing
301
+ fetchToolList,
302
+ fetchToolSchema,
303
+ generateExample,
304
+ };
@@ -34,7 +34,7 @@ const TOOL_ALIASES = {
34
34
  webagent: "web_agent",
35
35
  WebAgent: "web_agent",
36
36
  tinyfish: "web_agent",
37
- task: "fs_write",
37
+ task: "Task",
38
38
  write: "fs_write",
39
39
  filewrite: "fs_write",
40
40
  read: "fs_read",
@@ -77,6 +77,11 @@ const TOOL_CATEGORIES = {
77
77
  loader: () => require('./agent-task').registerAgentTaskTool,
78
78
  priority: 2,
79
79
  },
80
+ 'code-mode': {
81
+ keywords: ['mcp', 'execute', 'server', 'tool', 'code mode'],
82
+ loader: () => require('./code-mode').registerCodeModeTools,
83
+ priority: 3,
84
+ },
80
85
  };
81
86
 
82
87
  /**
@@ -281,6 +286,12 @@ function loadCategoryForTool(toolName) {
281
286
  'workspace_sandbox_sessions': 'mcp',
282
287
  'workspace_mcp_servers': 'mcp',
283
288
 
289
+ // Code Mode meta-tools
290
+ 'mcp_list_tools': 'code-mode',
291
+ 'mcp_tool_info': 'code-mode',
292
+ 'mcp_tool_docs': 'code-mode',
293
+ 'mcp_execute': 'code-mode',
294
+
284
295
  // Agent task
285
296
  // TinyFish (web agent)
286
297
  'web_agent': 'tinyfish',
@@ -1,5 +1,6 @@
1
1
  const { registerTool } = require(".");
2
2
  const { listServers, ensureClient } = require("../mcp");
3
+ const config = require("../config");
3
4
  const logger = require("../logger");
4
5
 
5
6
  const REMOTE_TOOL_PREFIX = "mcp";
@@ -12,6 +13,12 @@ function sanitiseName(value) {
12
13
  }
13
14
 
14
15
  async function registerRemoteTools() {
16
+ // Code Mode: register 4 meta-tools instead of individual remote tools
17
+ if (config.mcp?.codeMode?.enabled) {
18
+ const { registerCodeModeTools } = require("./code-mode");
19
+ registerCodeModeTools();
20
+ return;
21
+ }
15
22
  const servers = listServers();
16
23
  await Promise.all(
17
24
  servers.map(async (server) => {
@@ -316,6 +316,17 @@ function selectToolsSmartly(tools, classification, options = {}) {
316
316
  selectedTools = selectedTools.filter(t => minimalTools.includes(t.name));
317
317
  }
318
318
 
319
+ // Code Mode: always include the 4 meta-tools (only ~700 tokens total)
320
+ const codeConfig = require('../config');
321
+ if (codeConfig.mcp?.codeMode?.enabled) {
322
+ const codeModeNames = new Set(['mcp_list_tools', 'mcp_tool_info', 'mcp_tool_docs', 'mcp_execute']);
323
+ for (const tool of tools) {
324
+ if (codeModeNames.has(tool.name) && !selectedTools.some(t => t.name === tool.name)) {
325
+ selectedTools.push(tool);
326
+ }
327
+ }
328
+ }
329
+
319
330
  return selectedTools;
320
331
  }
321
332
 
package/src/tools/web.js CHANGED
@@ -152,7 +152,7 @@ function summariseResult(item) {
152
152
  return {
153
153
  title: item.title ?? item.name ?? null,
154
154
  url: item.url ?? item.link ?? null,
155
- snippet: item.snippet ?? item.summary ?? item.excerpt ?? null,
155
+ snippet: item.snippet ?? item.content ?? item.summary ?? item.excerpt ?? null,
156
156
  score: item.score ?? item.rank ?? null,
157
157
  source: item.source ?? null,
158
158
  metadata: item.metadata ?? null,
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Smart Payload Cloning & Size Estimation
3
+ *
4
+ * Optimizes deep-cloning of LLM request payloads to avoid
5
+ * wasting memory on large base64 media blocks that will be
6
+ * discarded by flattenBlocks() for most providers.
7
+ *
8
+ * @module utils/payload
9
+ */
10
+
11
+ const logger = require('../logger');
12
+
13
+ /**
14
+ * Estimate the byte size of message content without full serialization.
15
+ * Scans for base64 image/audio data blocks and text blocks.
16
+ *
17
+ * @param {Object} payload - Request payload
18
+ * @returns {number} Estimated size in bytes
19
+ */
20
+ function estimateContentSize(payload) {
21
+ if (!payload || !Array.isArray(payload.messages)) return 0;
22
+
23
+ let size = 0;
24
+ for (const msg of payload.messages) {
25
+ if (!msg) continue;
26
+
27
+ if (typeof msg.content === 'string') {
28
+ size += msg.content.length;
29
+ continue;
30
+ }
31
+
32
+ if (!Array.isArray(msg.content)) continue;
33
+
34
+ for (const block of msg.content) {
35
+ if (!block || typeof block !== 'object') continue;
36
+
37
+ if (block.text) {
38
+ size += block.text.length;
39
+ }
40
+ // Anthropic image format
41
+ if (block.source?.data) {
42
+ size += block.source.data.length;
43
+ }
44
+ // OpenAI image_url format (inline base64)
45
+ if (block.image_url?.url && block.image_url.url.startsWith('data:')) {
46
+ size += block.image_url.url.length;
47
+ }
48
+ // tool_result content
49
+ if (block.type === 'tool_result' && typeof block.content === 'string') {
50
+ size += block.content.length;
51
+ }
52
+ }
53
+ }
54
+
55
+ return size;
56
+ }
57
+
58
+ /**
59
+ * Check if any message content block has base64 media data exceeding threshold.
60
+ *
61
+ * @param {Object} payload - Request payload
62
+ * @param {number} threshold - Size threshold in bytes (default 1MB)
63
+ * @returns {boolean}
64
+ */
65
+ function hasLargeMedia(payload, threshold = 1_048_576) {
66
+ if (!payload || !Array.isArray(payload.messages)) return false;
67
+
68
+ for (const msg of payload.messages) {
69
+ if (!msg || !Array.isArray(msg.content)) continue;
70
+
71
+ for (const block of msg.content) {
72
+ if (!block || typeof block !== 'object') continue;
73
+
74
+ // Anthropic base64 image
75
+ if (block.source?.data && block.source.data.length > threshold) {
76
+ return true;
77
+ }
78
+ // OpenAI inline base64 image
79
+ if (block.image_url?.url && block.image_url.url.length > threshold) {
80
+ return true;
81
+ }
82
+ }
83
+ }
84
+
85
+ return false;
86
+ }
87
+
88
+ // Block types that flattenBlocks() discards (returns empty string)
89
+ const HEAVY_BLOCK_TYPES = new Set(['image', 'audio', 'image_url', 'video']);
90
+
91
+ /**
92
+ * Check if a content block is a heavy media block that flattenBlocks() discards.
93
+ * @param {Object} block
94
+ * @returns {boolean}
95
+ */
96
+ function isHeavyMediaBlock(block) {
97
+ if (!block || typeof block !== 'object') return false;
98
+ if (HEAVY_BLOCK_TYPES.has(block.type)) return true;
99
+ if (block.source?.type === 'base64') return true;
100
+ return false;
101
+ }
102
+
103
+ /**
104
+ * Clone payload optimized for providers that will flatten content.
105
+ * Skips cloning heavy media blocks since flattenBlocks() discards them.
106
+ *
107
+ * @param {Object} payload
108
+ * @returns {Object} Cloned payload with media placeholders
109
+ */
110
+ function cloneWithFlattenAwareness(payload) {
111
+ const clean = { ...payload };
112
+
113
+ // Deep-clone messages array but skip heavy media blocks
114
+ if (Array.isArray(payload.messages)) {
115
+ clean.messages = payload.messages.map(msg => {
116
+ if (!msg) return msg;
117
+ const cloned = { ...msg };
118
+
119
+ if (Array.isArray(msg.content)) {
120
+ cloned.content = msg.content.map(block => {
121
+ if (!block || typeof block !== 'object') return block;
122
+
123
+ // Skip heavy media blocks — flattenBlocks() produces "" for these
124
+ if (isHeavyMediaBlock(block)) {
125
+ return { type: block.type, _skipped: true };
126
+ }
127
+
128
+ // Shallow clone small blocks (text, tool_result, tool_use)
129
+ if (block.type === 'tool_result' && typeof block.content === 'object') {
130
+ return { ...block, content: JSON.parse(JSON.stringify(block.content)) };
131
+ }
132
+ return { ...block };
133
+ });
134
+ } else if (typeof msg.content === 'object' && msg.content !== null) {
135
+ cloned.content = { ...msg.content };
136
+ }
137
+
138
+ // Clone tool_calls if present
139
+ if (Array.isArray(msg.tool_calls)) {
140
+ cloned.tool_calls = JSON.parse(JSON.stringify(msg.tool_calls));
141
+ }
142
+
143
+ return cloned;
144
+ });
145
+ }
146
+
147
+ // Deep-clone small arrays that get mutated
148
+ if (Array.isArray(payload.tools)) {
149
+ clean.tools = JSON.parse(JSON.stringify(payload.tools));
150
+ }
151
+ if (Array.isArray(payload.system)) {
152
+ clean.system = JSON.parse(JSON.stringify(payload.system));
153
+ } else if (typeof payload.system === 'string') {
154
+ clean.system = payload.system;
155
+ }
156
+
157
+ return clean;
158
+ }
159
+
160
+ /**
161
+ * Smart deep-clone a request payload.
162
+ *
163
+ * - If willFlatten is true: skips cloning heavy media blocks (they'll be discarded)
164
+ * - If willFlatten is false: uses structuredClone (faster than JSON round-trip)
165
+ * - Falls back to JSON.parse(JSON.stringify()) for compatibility
166
+ *
167
+ * @param {Object} payload - Request payload to clone
168
+ * @param {Object} options
169
+ * @param {boolean} options.willFlatten - Whether flattenBlocks() will run (true for most providers)
170
+ * @returns {Object} Cloned payload
171
+ */
172
+ function clonePayloadSmart(payload, options = {}) {
173
+ if (!payload) return {};
174
+
175
+ const { willFlatten = false } = options;
176
+
177
+ // Fast path: provider will flatten content — skip cloning media blocks
178
+ if (willFlatten && Array.isArray(payload.messages)) {
179
+ const hasMedia = payload.messages.some(msg =>
180
+ Array.isArray(msg?.content) && msg.content.some(isHeavyMediaBlock)
181
+ );
182
+ if (hasMedia) {
183
+ logger.debug('[payload] Using flatten-aware clone (skipping media blocks)');
184
+ return cloneWithFlattenAwareness(payload);
185
+ }
186
+ }
187
+
188
+ // Medium path: structuredClone (faster, no string intermediate)
189
+ if (typeof structuredClone === 'function') {
190
+ try {
191
+ return structuredClone(payload);
192
+ } catch {
193
+ // structuredClone can fail on functions, symbols, etc.
194
+ }
195
+ }
196
+
197
+ // Slow path: JSON round-trip (original behavior)
198
+ return JSON.parse(JSON.stringify(payload));
199
+ }
200
+
201
+ module.exports = {
202
+ estimateContentSize,
203
+ hasLargeMedia,
204
+ clonePayloadSmart,
205
+ isHeavyMediaBlock,
206
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Request Performance Timer
3
+ *
4
+ * Lightweight timing instrumentation for the request hot path.
5
+ * Enable with LOG_LEVEL=debug or PERF_TIMER=true to see per-request
6
+ * breakdown of where time is spent.
7
+ *
8
+ * Usage:
9
+ * const timer = createTimer('processMessage');
10
+ * timer.mark('sanitizePayload');
11
+ * // ... do work ...
12
+ * timer.mark('cacheCheck');
13
+ * // ... do work ...
14
+ * timer.done(); // logs full breakdown
15
+ *
16
+ * @module utils/perf-timer
17
+ */
18
+
19
+ const { performance } = require('perf_hooks');
20
+ const logger = require('../logger');
21
+
22
+ const ENABLED = process.env.PERF_TIMER === 'true';
23
+
24
+ /**
25
+ * Create a performance timer for a named operation.
26
+ * @param {string} name - Timer name (e.g., 'processMessage', 'invokeModel')
27
+ * @returns {{ mark: (label: string) => void, done: () => Object }}
28
+ */
29
+ function createTimer(name) {
30
+ if (!ENABLED) {
31
+ // No-op when disabled — zero overhead
32
+ return {
33
+ mark() {},
34
+ done() { return null; },
35
+ };
36
+ }
37
+
38
+ const start = performance.now();
39
+ const marks = [];
40
+ let lastMark = start;
41
+
42
+ return {
43
+ /**
44
+ * Record a checkpoint.
45
+ * @param {string} label - What just completed
46
+ */
47
+ mark(label) {
48
+ const now = performance.now();
49
+ marks.push({
50
+ label,
51
+ elapsed: now - lastMark,
52
+ cumulative: now - start,
53
+ });
54
+ lastMark = now;
55
+ },
56
+
57
+ /**
58
+ * Finish timing and log the breakdown.
59
+ * @returns {Object} Timing breakdown
60
+ */
61
+ done() {
62
+ const total = performance.now() - start;
63
+ const breakdown = {};
64
+
65
+ for (const m of marks) {
66
+ breakdown[m.label] = `${m.elapsed.toFixed(2)}ms`;
67
+ }
68
+
69
+ logger.info({
70
+ timer: name,
71
+ totalMs: total.toFixed(2),
72
+ breakdown,
73
+ }, `[perf] ${name}: ${total.toFixed(1)}ms`);
74
+
75
+ return { name, totalMs: total, marks, breakdown };
76
+ },
77
+ };
78
+ }
79
+
80
+ module.exports = { createTimer };