lynkr 7.2.5 → 8.0.1

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 (124) hide show
  1. package/README.md +3 -3
  2. package/config/model-tiers.json +89 -0
  3. package/install.sh +6 -1
  4. package/package.json +4 -2
  5. package/scripts/setup.js +0 -1
  6. package/src/agents/executor.js +14 -6
  7. package/src/api/middleware/session.js +15 -2
  8. package/src/api/openai-router.js +162 -37
  9. package/src/api/providers-handler.js +15 -1
  10. package/src/api/router.js +107 -2
  11. package/src/budget/index.js +4 -3
  12. package/src/clients/databricks.js +431 -234
  13. package/src/clients/gpt-utils.js +181 -0
  14. package/src/clients/ollama-utils.js +66 -140
  15. package/src/clients/routing.js +0 -1
  16. package/src/clients/standard-tools.js +99 -3
  17. package/src/config/index.js +133 -35
  18. package/src/context/toon.js +173 -0
  19. package/src/logger/index.js +23 -0
  20. package/src/orchestrator/index.js +688 -213
  21. package/src/routing/agentic-detector.js +320 -0
  22. package/src/routing/complexity-analyzer.js +202 -2
  23. package/src/routing/cost-optimizer.js +305 -0
  24. package/src/routing/index.js +168 -159
  25. package/src/routing/model-tiers.js +365 -0
  26. package/src/server.js +4 -14
  27. package/src/sessions/cleanup.js +3 -3
  28. package/src/sessions/record.js +10 -1
  29. package/src/sessions/store.js +7 -2
  30. package/src/tools/agent-task.js +48 -1
  31. package/src/tools/index.js +19 -2
  32. package/src/tools/lazy-loader.js +7 -0
  33. package/src/tools/tinyfish.js +358 -0
  34. package/src/tools/truncate.js +1 -0
  35. package/.github/FUNDING.yml +0 -15
  36. package/.github/workflows/README.md +0 -215
  37. package/.github/workflows/ci.yml +0 -69
  38. package/.github/workflows/index.yml +0 -62
  39. package/.github/workflows/web-tools-tests.yml +0 -56
  40. package/CITATIONS.bib +0 -6
  41. package/CLAWROUTER_ROUTING_PLAN.md +0 -910
  42. package/DEPLOYMENT.md +0 -1001
  43. package/LYNKR-TUI-PLAN.md +0 -984
  44. package/PERFORMANCE-REPORT.md +0 -866
  45. package/PLAN-per-client-model-routing.md +0 -252
  46. package/ROUTER_COMPARISON.md +0 -173
  47. package/TIER_ROUTING_PLAN.md +0 -771
  48. package/docs/42642f749da6234f41b6b425c3bb07c9.txt +0 -1
  49. package/docs/BingSiteAuth.xml +0 -4
  50. package/docs/docs-style.css +0 -478
  51. package/docs/docs.html +0 -197
  52. package/docs/google5be250e608e6da39.html +0 -1
  53. package/docs/index.html +0 -577
  54. package/docs/index.md +0 -577
  55. package/docs/robots.txt +0 -4
  56. package/docs/sitemap.xml +0 -44
  57. package/docs/style.css +0 -1223
  58. package/documentation/README.md +0 -100
  59. package/documentation/api.md +0 -806
  60. package/documentation/claude-code-cli.md +0 -672
  61. package/documentation/codex-cli.md +0 -397
  62. package/documentation/contributing.md +0 -571
  63. package/documentation/cursor-integration.md +0 -731
  64. package/documentation/docker.md +0 -867
  65. package/documentation/embeddings.md +0 -760
  66. package/documentation/faq.md +0 -659
  67. package/documentation/features.md +0 -396
  68. package/documentation/headroom.md +0 -519
  69. package/documentation/installation.md +0 -706
  70. package/documentation/memory-system.md +0 -476
  71. package/documentation/production.md +0 -601
  72. package/documentation/providers.md +0 -906
  73. package/documentation/testing.md +0 -629
  74. package/documentation/token-optimization.md +0 -323
  75. package/documentation/tools.md +0 -697
  76. package/documentation/troubleshooting.md +0 -893
  77. package/final-test.js +0 -33
  78. package/headroom-sidecar/config.py +0 -93
  79. package/headroom-sidecar/requirements.txt +0 -14
  80. package/headroom-sidecar/server.py +0 -451
  81. package/monitor-agents.sh +0 -31
  82. package/scripts/audit-log-reader.js +0 -399
  83. package/scripts/compact-dictionary.js +0 -204
  84. package/scripts/test-deduplication.js +0 -448
  85. package/src/db/database.sqlite +0 -0
  86. package/test/README.md +0 -212
  87. package/test/azure-openai-config.test.js +0 -204
  88. package/test/azure-openai-error-resilience.test.js +0 -238
  89. package/test/azure-openai-format-conversion.test.js +0 -354
  90. package/test/azure-openai-integration.test.js +0 -281
  91. package/test/azure-openai-routing.test.js +0 -177
  92. package/test/azure-openai-streaming.test.js +0 -171
  93. package/test/bedrock-integration.test.js +0 -471
  94. package/test/comprehensive-test-suite.js +0 -928
  95. package/test/config-validation.test.js +0 -207
  96. package/test/cursor-integration.test.js +0 -484
  97. package/test/format-conversion.test.js +0 -578
  98. package/test/hybrid-routing-integration.test.js +0 -254
  99. package/test/hybrid-routing-performance.test.js +0 -418
  100. package/test/llamacpp-integration.test.js +0 -863
  101. package/test/lmstudio-integration.test.js +0 -335
  102. package/test/memory/extractor.test.js +0 -398
  103. package/test/memory/retriever.test.js +0 -613
  104. package/test/memory/retriever.test.js.bak +0 -585
  105. package/test/memory/search.test.js +0 -537
  106. package/test/memory/search.test.js.bak +0 -389
  107. package/test/memory/store.test.js +0 -344
  108. package/test/memory/store.test.js.bak +0 -312
  109. package/test/memory/surprise.test.js +0 -300
  110. package/test/memory-performance.test.js +0 -472
  111. package/test/openai-integration.test.js +0 -686
  112. package/test/openrouter-error-resilience.test.js +0 -418
  113. package/test/passthrough-mode.test.js +0 -385
  114. package/test/performance-benchmark.js +0 -351
  115. package/test/performance-tests.js +0 -528
  116. package/test/routing.test.js +0 -219
  117. package/test/web-tools.test.js +0 -329
  118. package/test-agents-simple.js +0 -43
  119. package/test-cli-connection.sh +0 -33
  120. package/test-learning-unit.js +0 -126
  121. package/test-learning.js +0 -112
  122. package/test-parallel-agents.sh +0 -124
  123. package/test-parallel-direct.js +0 -155
  124. package/test-subagents.sh +0 -117
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Model Tier Selector
3
+ * Maps complexity scores to appropriate models per provider
4
+ * Uses config/model-tiers.json for tier preferences
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const logger = require('../logger');
10
+ const config = require('../config');
11
+
12
+ // Load tier config
13
+ const TIER_CONFIG_PATH = path.join(__dirname, '../../config/model-tiers.json');
14
+
15
+ // Tier definitions with complexity ranges
16
+ const TIER_DEFINITIONS = {
17
+ SIMPLE: {
18
+ description: 'Greetings, simple Q&A, confirmations',
19
+ range: [0, 25],
20
+ priority: 1,
21
+ },
22
+ MEDIUM: {
23
+ description: 'Code reading, simple edits, research',
24
+ range: [26, 50],
25
+ priority: 2,
26
+ },
27
+ COMPLEX: {
28
+ description: 'Multi-file changes, debugging, architecture',
29
+ range: [51, 75],
30
+ priority: 3,
31
+ },
32
+ REASONING: {
33
+ description: 'Complex analysis, security audits, novel problems',
34
+ range: [76, 100],
35
+ priority: 4,
36
+ },
37
+ };
38
+
39
+ class ModelTierSelector {
40
+ constructor() {
41
+ this.tierConfig = null;
42
+ this.localProviders = {};
43
+ this.providerAliases = {};
44
+ this._loadConfig();
45
+ }
46
+
47
+ /**
48
+ * Load tier configuration from JSON file
49
+ */
50
+ _loadConfig() {
51
+ try {
52
+ if (fs.existsSync(TIER_CONFIG_PATH)) {
53
+ const data = JSON.parse(fs.readFileSync(TIER_CONFIG_PATH, 'utf8'));
54
+ this.tierConfig = data.tiers || {};
55
+ this.localProviders = data.localProviders || {};
56
+ this.providerAliases = data.providerAliases || {};
57
+ logger.debug({ tiers: Object.keys(this.tierConfig) }, '[ModelTiers] Config loaded');
58
+ } else {
59
+ logger.warn('[ModelTiers] Config file not found, using defaults');
60
+ this._loadDefaults();
61
+ }
62
+ } catch (err) {
63
+ logger.warn({ err: err.message }, '[ModelTiers] Config load failed, using defaults');
64
+ this._loadDefaults();
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Load default tier config
70
+ */
71
+ _loadDefaults() {
72
+ this.tierConfig = {
73
+ SIMPLE: { preferred: { ollama: ['llama3.2'], openai: ['gpt-4o-mini'] } },
74
+ MEDIUM: { preferred: { openai: ['gpt-4o'], anthropic: ['claude-sonnet-4-20250514'] } },
75
+ COMPLEX: { preferred: { openai: ['o1-mini'], anthropic: ['claude-sonnet-4-20250514'] } },
76
+ REASONING: { preferred: { openai: ['o1'], anthropic: ['claude-opus-4-20250514'] } },
77
+ };
78
+ this.localProviders = {
79
+ ollama: { free: true, defaultTier: 'SIMPLE' },
80
+ llamacpp: { free: true, defaultTier: 'SIMPLE' },
81
+ lmstudio: { free: true, defaultTier: 'SIMPLE' },
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Normalize provider name using aliases
87
+ */
88
+ _normalizeProvider(provider) {
89
+ if (!provider) return 'openai';
90
+ const lower = provider.toLowerCase();
91
+ return this.providerAliases[lower] || lower;
92
+ }
93
+
94
+ /**
95
+ * Get tier from complexity score
96
+ * @param {number} complexityScore - Score from 0-100
97
+ * @returns {string} Tier name (SIMPLE, MEDIUM, COMPLEX, REASONING)
98
+ */
99
+ getTier(complexityScore) {
100
+ const score = Math.max(0, Math.min(100, complexityScore || 0));
101
+
102
+ for (const [tier, def] of Object.entries(TIER_DEFINITIONS)) {
103
+ if (score >= def.range[0] && score <= def.range[1]) {
104
+ return tier;
105
+ }
106
+ }
107
+
108
+ return score > 75 ? 'REASONING' : 'SIMPLE';
109
+ }
110
+
111
+ /**
112
+ * Get tier definition
113
+ */
114
+ getTierDefinition(tier) {
115
+ return TIER_DEFINITIONS[tier] || TIER_DEFINITIONS.MEDIUM;
116
+ }
117
+
118
+ /**
119
+ * Get tier priority (1-4)
120
+ */
121
+ getTierPriority(tier) {
122
+ return TIER_DEFINITIONS[tier]?.priority || 2;
123
+ }
124
+
125
+ /**
126
+ * Compare two tiers, returns positive if tier1 > tier2
127
+ */
128
+ compareTiers(tier1, tier2) {
129
+ return this.getTierPriority(tier1) - this.getTierPriority(tier2);
130
+ }
131
+
132
+ /**
133
+ * Get preferred models for a tier and provider
134
+ * @param {string} tier - Tier name
135
+ * @param {string} provider - Provider name
136
+ * @returns {string[]} Array of model names
137
+ */
138
+ getPreferredModels(tier, provider) {
139
+ const normalizedProvider = this._normalizeProvider(provider);
140
+ return this.tierConfig[tier]?.preferred?.[normalizedProvider] || [];
141
+ }
142
+
143
+ /**
144
+ * Select model for tier from TIER_* env var (mandatory)
145
+ * @param {string} tier - Tier name (SIMPLE, MEDIUM, COMPLEX, REASONING)
146
+ * @param {string} _unused - Deprecated parameter
147
+ * @returns {Object} { model, provider, source, tier }
148
+ */
149
+ selectModel(tier, _unused = null) {
150
+ const tierConfig = config.modelTiers?.[tier];
151
+ if (!tierConfig) {
152
+ throw new Error(`TIER_${tier} not configured. Set TIER_${tier}=provider:model in .env`);
153
+ }
154
+
155
+ const parsed = this._parseTierConfig(tierConfig);
156
+ if (!parsed) {
157
+ throw new Error(`Invalid TIER_${tier} format. Expected provider:model, got: ${tierConfig}`);
158
+ }
159
+
160
+ return {
161
+ model: parsed.model,
162
+ provider: parsed.provider,
163
+ source: 'env_tier',
164
+ tier,
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Parse tier config string (format: provider:model)
170
+ * Examples: "ollama:llama3.2", "azure-openai:gpt-5.2-chat", "openai:gpt-4o"
171
+ */
172
+ _parseTierConfig(configStr) {
173
+ if (!configStr || typeof configStr !== 'string') return null;
174
+
175
+ const colonIndex = configStr.indexOf(':');
176
+ if (colonIndex === -1) {
177
+ // No colon - treat as model name, use default provider
178
+ return {
179
+ provider: config.modelProvider?.type || 'openai',
180
+ model: configStr.trim(),
181
+ };
182
+ }
183
+
184
+ const provider = configStr.substring(0, colonIndex).trim().toLowerCase();
185
+ const model = configStr.substring(colonIndex + 1).trim();
186
+
187
+ if (!provider || !model) return null;
188
+
189
+ return { provider, model };
190
+ }
191
+
192
+ /**
193
+ * Get the model configured for a provider from .env
194
+ */
195
+ _getProviderModel(provider) {
196
+ switch (provider) {
197
+ case 'azure-openai':
198
+ case 'azureopenai':
199
+ return config.azureOpenAI?.deployment || null;
200
+ case 'openai':
201
+ return config.openai?.model || null;
202
+ case 'ollama':
203
+ return config.ollama?.model || null;
204
+ case 'openrouter':
205
+ return config.openrouter?.model || null;
206
+ case 'llamacpp':
207
+ return config.llamacpp?.model || null;
208
+ case 'lmstudio':
209
+ return config.lmstudio?.model || null;
210
+ case 'bedrock':
211
+ return config.bedrock?.modelId || null;
212
+ case 'zai':
213
+ return config.zai?.model || null;
214
+ case 'moonshot':
215
+ return config.moonshot?.model || null;
216
+ case 'vertex':
217
+ return config.vertex?.model || null;
218
+ case 'databricks':
219
+ return config.modelProvider?.defaultModel || null;
220
+ default:
221
+ return null;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Get provider for a specific tier (from env or fallback)
227
+ */
228
+ getProviderForTier(tier) {
229
+ const tierConfig = config.modelTiers?.[tier];
230
+ if (tierConfig) {
231
+ const parsed = this._parseTierConfig(tierConfig);
232
+ if (parsed) return parsed.provider;
233
+ }
234
+ return config.modelProvider?.type || 'openai';
235
+ }
236
+
237
+ /**
238
+ * Get fallback model if provider can't handle requested tier
239
+ */
240
+ _getFallbackModel(requestedTier, provider) {
241
+ const tierOrder = ['REASONING', 'COMPLEX', 'MEDIUM', 'SIMPLE'];
242
+ const startIndex = tierOrder.indexOf(requestedTier);
243
+
244
+ // Try lower tiers
245
+ for (let i = startIndex + 1; i < tierOrder.length; i++) {
246
+ const fallbackTier = tierOrder[i];
247
+ const models = this.getPreferredModels(fallbackTier, provider);
248
+
249
+ if (models.length > 0) {
250
+ logger.debug({
251
+ from: requestedTier,
252
+ to: fallbackTier,
253
+ provider,
254
+ model: models[0],
255
+ }, '[ModelTiers] Downgrading tier');
256
+
257
+ return { model: models[0], tier: fallbackTier };
258
+ }
259
+ }
260
+
261
+ return null;
262
+ }
263
+
264
+ /**
265
+ * Check if provider can handle a specific tier
266
+ */
267
+ canHandleTier(provider, tier) {
268
+ const normalizedProvider = this._normalizeProvider(provider);
269
+ const models = this.getPreferredModels(tier, normalizedProvider);
270
+ return models.length > 0;
271
+ }
272
+
273
+ /**
274
+ * Check if provider is local/free
275
+ */
276
+ isLocalProvider(provider) {
277
+ const normalizedProvider = this._normalizeProvider(provider);
278
+ return this.localProviders[normalizedProvider]?.free === true;
279
+ }
280
+
281
+ /**
282
+ * Get all providers that can handle a tier
283
+ */
284
+ getProvidersForTier(tier) {
285
+ const tierConfig = this.tierConfig[tier];
286
+ if (!tierConfig?.preferred) return [];
287
+ return Object.keys(tierConfig.preferred);
288
+ }
289
+
290
+ /**
291
+ * Get all tiers a provider can handle
292
+ */
293
+ getTiersForProvider(provider) {
294
+ const normalizedProvider = this._normalizeProvider(provider);
295
+ const tiers = [];
296
+
297
+ for (const tier of Object.keys(TIER_DEFINITIONS)) {
298
+ if (this.canHandleTier(normalizedProvider, tier)) {
299
+ tiers.push(tier);
300
+ }
301
+ }
302
+
303
+ return tiers;
304
+ }
305
+
306
+ /**
307
+ * Get tier stats for metrics endpoint
308
+ */
309
+ getTierStats() {
310
+ const stats = {
311
+ tiers: {},
312
+ providers: {},
313
+ };
314
+
315
+ for (const [tier, def] of Object.entries(TIER_DEFINITIONS)) {
316
+ const providers = this.getProvidersForTier(tier);
317
+ stats.tiers[tier] = {
318
+ ...def,
319
+ providerCount: providers.length,
320
+ providers: providers,
321
+ };
322
+ }
323
+
324
+ // Count models per provider
325
+ const allProviders = new Set();
326
+ for (const tierConfig of Object.values(this.tierConfig)) {
327
+ if (tierConfig.preferred) {
328
+ Object.keys(tierConfig.preferred).forEach(p => allProviders.add(p));
329
+ }
330
+ }
331
+
332
+ for (const provider of allProviders) {
333
+ stats.providers[provider] = {
334
+ tiers: this.getTiersForProvider(provider),
335
+ isLocal: this.isLocalProvider(provider),
336
+ };
337
+ }
338
+
339
+ return stats;
340
+ }
341
+
342
+ /**
343
+ * Reload configuration (for hot reload)
344
+ */
345
+ reload() {
346
+ this._loadConfig();
347
+ logger.info('[ModelTiers] Configuration reloaded');
348
+ }
349
+ }
350
+
351
+ // Singleton instance
352
+ let instance = null;
353
+
354
+ function getModelTierSelector() {
355
+ if (!instance) {
356
+ instance = new ModelTierSelector();
357
+ }
358
+ return instance;
359
+ }
360
+
361
+ module.exports = {
362
+ ModelTierSelector,
363
+ getModelTierSelector,
364
+ TIER_DEFINITIONS,
365
+ };
package/src/server.js CHANGED
@@ -78,18 +78,8 @@ function createApp() {
78
78
  // Metrics collection
79
79
  app.use(metricsMiddleware);
80
80
 
81
- // Enable compression for all responses (gzip/deflate)
82
- app.use(compression({
83
- level: 6, // Balanced compression level
84
- threshold: 1024, // Only compress responses > 1KB
85
- filter: (req, res) => {
86
- // Don't compress event streams
87
- if (res.getHeader('Content-Type') === 'text/event-stream') {
88
- return false;
89
- }
90
- return compression.filter(req, res);
91
- }
92
- }));
81
+ // Note: If using a tunnel (ngrok, Cloudflare Tunnel) and seeing BrotliDecompressionError,
82
+ // start ngrok with: ngrok http 8081 --request-header-remove "Accept-Encoding"
93
83
 
94
84
  app.use(express.json({ limit: config.server.jsonLimit }));
95
85
  app.use(sessionMiddleware);
@@ -201,9 +191,9 @@ async function start() {
201
191
 
202
192
  const app = createApp();
203
193
 
204
- // Wait for Ollama if it's the configured provider or preferred for routing
194
+ // Wait for Ollama if it's the configured provider or referenced in tier config
205
195
  const provider = config.modelProvider?.type?.toLowerCase();
206
- if (provider === "ollama" || config.modelProvider?.preferOllama) {
196
+ if (provider === "ollama" || config.tiersReferenceOllama()) {
207
197
  await waitForOllama();
208
198
  }
209
199
 
@@ -4,9 +4,9 @@ const { cleanupOldSessions, cleanupOldHistory } = require("./store");
4
4
  class SessionCleanupManager {
5
5
  constructor(options = {}) {
6
6
  this.enabled = options.enabled !== false;
7
- this.intervalMs = options.intervalMs || 3600000; // 1 hour
8
- this.sessionMaxAgeMs = options.sessionMaxAgeMs || 7 * 24 * 60 * 60 * 1000; // 7 days
9
- this.historyMaxAgeMs = options.historyMaxAgeMs || 30 * 24 * 60 * 60 * 1000; // 30 days
7
+ this.intervalMs = options.intervalMs || 300000; // 5 minutes (was 1 hour)
8
+ this.sessionMaxAgeMs = options.sessionMaxAgeMs || 24 * 60 * 60 * 1000; // 1 day (was 7 days)
9
+ this.historyMaxAgeMs = options.historyMaxAgeMs || 7 * 24 * 60 * 60 * 1000; // 7 days (was 30 days)
10
10
  this.timer = null;
11
11
  }
12
12
 
@@ -1,5 +1,8 @@
1
1
  const { appendSessionTurn } = require("./store");
2
2
 
3
+ // Cap in-memory history to prevent unbounded growth during long tool loops
4
+ const MAX_IN_MEMORY_HISTORY = 100;
5
+
3
6
  function ensureSessionShape(session) {
4
7
  if (!session) return null;
5
8
  if (!Array.isArray(session.history)) {
@@ -19,7 +22,13 @@ function appendTurnToSession(session, entry) {
19
22
  target.history.push(turn);
20
23
  target.updatedAt = turn.timestamp;
21
24
 
22
- if (target.id) {
25
+ // Trim in-memory history if it exceeds the cap
26
+ if (target.history.length > MAX_IN_MEMORY_HISTORY) {
27
+ target.history = target.history.slice(-MAX_IN_MEMORY_HISTORY);
28
+ }
29
+
30
+ // Skip DB write for ephemeral sessions (auto-generated, no client session ID)
31
+ if (target.id && !target._ephemeral) {
23
32
  appendSessionTurn(target.id, turn, target.metadata ?? {});
24
33
  }
25
34
 
@@ -4,11 +4,15 @@ const logger = require("../logger");
4
4
  const selectSessionStmt = db.prepare(
5
5
  "SELECT id, created_at, updated_at, metadata FROM sessions WHERE id = ?",
6
6
  );
7
+ // Limit history to last 50 entries to prevent unbounded memory growth.
8
+ // Older entries remain in DB for auditing but aren't loaded into memory.
9
+ const MAX_HISTORY_ROWS = 50;
7
10
  const selectHistoryStmt = db.prepare(
8
11
  `SELECT role, type, status, content, metadata, timestamp
9
12
  FROM session_history
10
13
  WHERE session_id = ?
11
- ORDER BY timestamp ASC, id ASC`,
14
+ ORDER BY timestamp DESC, id DESC
15
+ LIMIT ${MAX_HISTORY_ROWS}`,
12
16
  );
13
17
  const insertSessionStmt = db.prepare(
14
18
  "INSERT INTO sessions (id, created_at, updated_at, metadata) VALUES (@id, @created_at, @updated_at, @metadata)",
@@ -75,7 +79,8 @@ function getSession(sessionId) {
75
79
  if (!sessionId) return null;
76
80
  const sessionRow = selectSessionStmt.get(sessionId);
77
81
  if (!sessionRow) return null;
78
- const historyRows = selectHistoryStmt.all(sessionId);
82
+ // Query returns rows in DESC order (for LIMIT to grab newest), reverse to ASC
83
+ const historyRows = selectHistoryStmt.all(sessionId).reverse();
79
84
  return toSession(sessionRow, historyRows);
80
85
  }
81
86
 
@@ -2,6 +2,50 @@ const { registerTool } = require(".");
2
2
  const { spawnAgent, autoSelectAgent } = require("../agents");
3
3
  const logger = require("../logger");
4
4
 
5
+ /**
6
+ * Extract text from Anthropic content blocks format
7
+ * Handles: [{"type":"text","text":"..."}] -> "..."
8
+ */
9
+ function extractTextFromContentBlocks(content) {
10
+ if (typeof content !== 'string') {
11
+ return content;
12
+ }
13
+
14
+ const trimmed = content.trim();
15
+ if (!trimmed.startsWith('[')) {
16
+ return content;
17
+ }
18
+
19
+ try {
20
+ const parsed = JSON.parse(trimmed);
21
+ if (!Array.isArray(parsed)) {
22
+ return content;
23
+ }
24
+
25
+ // Extract text from content blocks
26
+ const textParts = parsed
27
+ .filter(block => block && typeof block === 'object')
28
+ .map(block => {
29
+ if (block.type === 'text' && typeof block.text === 'string') {
30
+ return block.text;
31
+ }
32
+ if (typeof block.text === 'string') {
33
+ return block.text;
34
+ }
35
+ return null;
36
+ })
37
+ .filter(text => text !== null);
38
+
39
+ if (textParts.length > 0) {
40
+ return textParts.join('\n\n');
41
+ }
42
+
43
+ return content;
44
+ } catch {
45
+ return content;
46
+ }
47
+ }
48
+
5
49
  function registerAgentTaskTool() {
6
50
  registerTool(
7
51
  "Task",
@@ -49,10 +93,13 @@ function registerAgentTaskTool() {
49
93
  });
50
94
 
51
95
  if (result.success) {
96
+ // Extract text from Anthropic content blocks if present
97
+ const cleanContent = extractTextFromContentBlocks(result.result);
98
+
52
99
  return {
53
100
  ok: true,
54
101
  status: 200,
55
- content: result.result,
102
+ content: cleanContent,
56
103
  metadata: {
57
104
  agentType: subagentType,
58
105
  agentId: result.stats.agentId,
@@ -1,5 +1,6 @@
1
1
  const logger = require("../logger");
2
2
  const { truncateToolOutput } = require("./truncate");
3
+ const { isGPTProvider, formatToolResultForGPT } = require("../clients/gpt-utils");
3
4
 
4
5
  const registry = new Map();
5
6
  const registryLowercase = new Map();
@@ -29,6 +30,10 @@ const TOOL_ALIASES = {
29
30
  WebSearch: "web_search",
30
31
  web_fetch: "web_fetch",
31
32
  webfetch: "web_fetch",
33
+ web_agent: "web_agent",
34
+ webagent: "web_agent",
35
+ WebAgent: "web_agent",
36
+ tinyfish: "web_agent",
32
37
  task: "fs_write",
33
38
  write: "fs_write",
34
39
  filewrite: "fs_write",
@@ -254,7 +259,18 @@ async function executeToolCall(call, context = {}) {
254
259
  const formatted = normalizeHandlerResult(result);
255
260
 
256
261
  // Apply tool output truncation for token efficiency
257
- const truncatedContent = truncateToolOutput(normalisedCall.name, formatted.content);
262
+ let truncatedContent = truncateToolOutput(normalisedCall.name, formatted.content);
263
+
264
+ // GPT-specific formatting temporarily disabled for testing
265
+ // const isGPT = context?.provider && isGPTProvider(context.provider);
266
+ // if (isGPT) {
267
+ // truncatedContent = formatToolResultForGPT(
268
+ // normalisedCall.name,
269
+ // truncatedContent,
270
+ // normalisedCall.arguments
271
+ // );
272
+ // }
273
+ const isGPT = false; // Disabled for testing
258
274
 
259
275
  return {
260
276
  id: normalisedCall.id,
@@ -267,7 +283,8 @@ async function executeToolCall(call, context = {}) {
267
283
  registered: true,
268
284
  truncated: truncatedContent !== formatted.content,
269
285
  originalLength: formatted.content?.length,
270
- truncatedLength: truncatedContent?.length
286
+ truncatedLength: truncatedContent?.length,
287
+ gptFormatted: isGPT,
271
288
  },
272
289
  };
273
290
  } catch (err) {
@@ -57,6 +57,11 @@ const TOOL_CATEGORIES = {
57
57
  loader: () => require('./tasks').registerTaskTools,
58
58
  priority: 2,
59
59
  },
60
+ tinyfish: {
61
+ keywords: ['tinyfish', 'web_agent', 'automate', 'scrape', 'extract', 'crawl', 'browser'],
62
+ loader: () => require('./tinyfish').registerTinyFishTools,
63
+ priority: 2,
64
+ },
60
65
  tests: {
61
66
  keywords: ['test', 'jest', 'mocha', 'pytest', 'unittest', 'spec', 'coverage', 'assert'],
62
67
  loader: () => require('./tests').registerTestTools,
@@ -277,6 +282,8 @@ function loadCategoryForTool(toolName) {
277
282
  'workspace_mcp_servers': 'mcp',
278
283
 
279
284
  // Agent task
285
+ // TinyFish (web agent)
286
+ 'web_agent': 'tinyfish',
280
287
  'agent_task': 'agentTask',
281
288
  };
282
289