@johpaz/hive-sdk 0.0.18 → 0.1.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.
Files changed (41) hide show
  1. package/bun.lock +291 -1
  2. package/docs/HIVE-HARNESS.md +113 -0
  3. package/package.json +36 -2
  4. package/packages/cli/package.json +1 -1
  5. package/packages/core/package.json +13 -2
  6. package/packages/core/src/ace/Tracer.ts +1 -1
  7. package/packages/core/src/agent/AgentRunner.ts +12 -0
  8. package/packages/core/src/agent/ContextCompiler.ts +4 -4
  9. package/packages/core/src/agent/ConversationStore.ts +30 -20
  10. package/packages/core/src/agent/selectors/PlaybookSelector.ts +50 -76
  11. package/packages/core/src/agent/selectors/SkillSelector.ts +106 -262
  12. package/packages/core/src/agent/selectors/ToolSelector.ts +53 -89
  13. package/packages/core/src/auth/auth.ts +36 -23
  14. package/packages/core/src/harness/boot-id.ts +20 -0
  15. package/packages/core/src/harness/collections.ts +98 -0
  16. package/packages/core/src/harness/db-helpers.ts +87 -0
  17. package/packages/core/src/harness/durable-queue.ts +337 -0
  18. package/packages/core/src/harness/goal-verifier.ts +141 -0
  19. package/packages/core/src/harness/harness.test.ts +236 -0
  20. package/packages/core/src/harness/index.ts +34 -0
  21. package/packages/core/src/harness/job-store.ts +399 -0
  22. package/packages/core/src/harness/proof-packet.ts +69 -0
  23. package/packages/core/src/harness/reconcile.ts +149 -0
  24. package/packages/core/src/harness/run-epoch.ts +32 -0
  25. package/packages/core/src/harness/run-store.ts +334 -0
  26. package/packages/core/src/index.ts +13 -0
  27. package/packages/core/src/memory/Scratchpad.test.ts +23 -21
  28. package/packages/core/src/memory/Scratchpad.ts +41 -24
  29. package/packages/core/src/storage/HiveDBStorage.ts +64 -0
  30. package/packages/core/src/storage/SQLiteStorage.ts +7 -0
  31. package/packages/core/src/storage/hiveSeed.ts +308 -0
  32. package/packages/core/src/storage/hiveStorage.test.ts +38 -0
  33. package/packages/core/src/storage/index.ts +11 -0
  34. package/packages/core/src/storage/seed.ts +5 -1
  35. package/packages/core/src/storage/usage.ts +106 -167
  36. package/packages/core/src/tool-runtime/tool-runtime.test.ts +11 -3
  37. package/packages/core/src/tools/agents/get-available-models.ts +52 -56
  38. package/packages/core/src/tools/agents/index.ts +77 -60
  39. package/packages/core/src/tools/core/index.ts +106 -291
  40. package/packages/core/src/tools/meeting/index.ts +83 -93
  41. package/packages/core/src/utils/toon.ts +4 -4
@@ -1,20 +1,15 @@
1
- import { getDb } from "./SQLiteStorage.ts";
1
+ import { getHiveDB } from "./HiveDBStorage.ts";
2
2
  import { randomUUID } from "crypto";
3
3
  import { logger } from "../utils/logger.ts";
4
4
 
5
5
  const log = logger.child("usage");
6
6
 
7
- // Precios en USD por millón de tokens (input / output)
8
- // Fuentes: docs.anthropic.com, openrouter.ai/api/v1/models, api-docs.deepseek.com, console.groq.com
9
7
  const MODEL_PRICING: Record<string, { inputPer1M: number; outputPer1M: number }> = {
10
- // ── Anthropic (fuente: docs.anthropic.com) ──
11
8
  "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25 },
12
9
  "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15 },
13
10
  "claude-haiku-4-5-20251001": { inputPer1M: 1, outputPer1M: 5 },
14
11
  "anthropic/claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25 },
15
12
  "anthropic/claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15 },
16
-
17
- // ── OpenAI (fuente: openrouter.ai/api/v1/models) ──
18
13
  "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10 },
19
14
  "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6 },
20
15
  "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15 },
@@ -25,11 +20,8 @@ const MODEL_PRICING: Record<string, { inputPer1M: number; outputPer1M: number }>
25
20
  "openai/gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15 },
26
21
  "openai/gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180 },
27
22
  "openai/gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14 },
28
- // Groq OSS (fuente: console.groq.com)
29
23
  "openai/gpt-oss-120b": { inputPer1M: 0.15, outputPer1M: 0.6 },
30
24
  "openai/gpt-oss-20b": { inputPer1M: 0.075, outputPer1M: 0.3 },
31
-
32
- // ── Google Gemini (fuente: openrouter.ai/api/v1/models) ──
33
25
  "gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12 },
34
26
  "gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5 },
35
27
  "gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3 },
@@ -41,8 +33,6 @@ const MODEL_PRICING: Record<string, { inputPer1M: number; outputPer1M: number }>
41
33
  "google/gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5 },
42
34
  "google/gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3 },
43
35
  "google/gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6 },
44
-
45
- // ── Mistral (fuente: openrouter.ai/api/v1/models) ──
46
36
  "mistral-large-2512": { inputPer1M: 0.5, outputPer1M: 1.5 },
47
37
  "devstral-2512": { inputPer1M: 0.4, outputPer1M: 2 },
48
38
  "ministral-14b-2512": { inputPer1M: 0.2, outputPer1M: 0.2 },
@@ -51,14 +41,10 @@ const MODEL_PRICING: Record<string, { inputPer1M: number; outputPer1M: number }>
51
41
  "mistral-small-3.2-24b-instruct": { inputPer1M: 0.1, outputPer1M: 0.3 },
52
42
  "mistral-large-latest": { inputPer1M: 0.5, outputPer1M: 1.5 },
53
43
  "codestral-latest": { inputPer1M: 0.2, outputPer1M: 0.6 },
54
-
55
- // ── DeepSeek (fuente: api-docs.deepseek.com/quick_start/pricing) ──
56
44
  "deepseek-chat": { inputPer1M: 0.28, outputPer1M: 0.42 },
57
45
  "deepseek-reasoner": { inputPer1M: 0.28, outputPer1M: 0.42 },
58
46
  "deepseek/deepseek-v3.2": { inputPer1M: 0.25, outputPer1M: 0.4 },
59
47
  "deepseek/deepseek-r1:free": { inputPer1M: 0, outputPer1M: 0 },
60
-
61
- // ── Kimi / Moonshot (fuente: openrouter.ai/moonshotai) ──
62
48
  "kimi-k2.5": { inputPer1M: 0.45, outputPer1M: 2.2 },
63
49
  "kimi-k2": { inputPer1M: 0.45, outputPer1M: 2.2 },
64
50
  "moonshot-v1-8k": { inputPer1M: 1.67, outputPer1M: 1.67 },
@@ -66,23 +52,15 @@ const MODEL_PRICING: Record<string, { inputPer1M: number; outputPer1M: number }>
66
52
  "moonshot-v1-128k": { inputPer1M: 8.33, outputPer1M: 8.33 },
67
53
  "moonshotai/kimi-k2.5": { inputPer1M: 0.45, outputPer1M: 2.2 },
68
54
  "moonshotai/kimi-k2-instruct-0905": { inputPer1M: 0.45, outputPer1M: 2.2 },
69
-
70
- // ── Meta Llama (vía OpenRouter) ──
71
55
  "meta-llama/llama-3.3-70b-instruct": { inputPer1M: 0.88, outputPer1M: 0.88 },
72
56
  "meta-llama/llama-4-maverick": { inputPer1M: 0.2, outputPer1M: 0.8 },
73
-
74
- // ── Qwen (vía OpenRouter) ──
75
57
  "qwen/qwen3.5-plus-02-15": { inputPer1M: 0.26, outputPer1M: 1.56 },
76
58
  "qwen/qwen3.5-flash-02-23": { inputPer1M: 0.1, outputPer1M: 0.4 },
77
59
  "qwen/qwen3-32b": { inputPer1M: 0, outputPer1M: 0 },
78
-
79
- // ── Groq (fuente: console.groq.com/docs/models) ──
80
60
  "llama-3.3-70b-versatile": { inputPer1M: 0.59, outputPer1M: 0.79 },
81
61
  "llama-3.1-8b-instant": { inputPer1M: 0.05, outputPer1M: 0.08 },
82
62
  "groq/compound": { inputPer1M: 0, outputPer1M: 0 },
83
63
  "groq/compound-mini": { inputPer1M: 0, outputPer1M: 0 },
84
-
85
- // ── Ollama local = siempre free ──
86
64
  "qwen3:4b": { inputPer1M: 0, outputPer1M: 0 },
87
65
  "qwen3:8b": { inputPer1M: 0, outputPer1M: 0 },
88
66
  "qwen3:14b": { inputPer1M: 0, outputPer1M: 0 },
@@ -134,117 +112,101 @@ export interface UsageSummary {
134
112
  recentRecords: UsageRecord[];
135
113
  }
136
114
 
137
- export function recordUsage(options: {
115
+ export async function recordUsage(options: {
138
116
  provider: string;
139
117
  model: string;
140
118
  inputTokens: number;
141
119
  outputTokens: number;
142
120
  latencyMs?: number;
143
- }): void {
121
+ }): Promise<void> {
144
122
  try {
145
- const db = getDb();
123
+ const db = await getHiveDB();
124
+ const col = db.collection<UsageRecord>("usage_records");
146
125
  const costUsd = calculateCost(options.model, options.inputTokens, options.outputTokens);
147
126
 
148
- db.prepare(`
149
- INSERT INTO usage_records (id, provider, model, input_tokens, output_tokens, cost_usd, latency_ms, created_at)
150
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
151
- `).run(
152
- randomUUID(),
153
- options.provider,
154
- options.model,
155
- options.inputTokens,
156
- options.outputTokens,
157
- costUsd,
158
- options.latencyMs || null,
159
- Math.floor(Date.now() / 1000)
160
- );
127
+ await col.put(randomUUID(), {
128
+ id: randomUUID(),
129
+ provider: options.provider,
130
+ model: options.model,
131
+ input_tokens: options.inputTokens,
132
+ output_tokens: options.outputTokens,
133
+ cost_usd: costUsd,
134
+ latency_ms: options.latencyMs ?? null,
135
+ toon_saved_tokens: 0,
136
+ toon_saved_cost: 0,
137
+ toon_json_bytes: 0,
138
+ toon_toon_bytes: 0,
139
+ toon_saved_bytes: 0,
140
+ toon_saved_percent: 0,
141
+ toon_json_tokens: 0,
142
+ toon_toon_tokens: 0,
143
+ toon_saved_tokens_pct: 0,
144
+ created_at: Math.floor(Date.now() / 1000),
145
+ });
146
+
161
147
  log.info(`[USAGE RECORDED] provider=${options.provider} model=${options.model} input=${options.inputTokens} output=${options.outputTokens} cost=$${costUsd.toFixed(4)}`);
162
148
  } catch (error) {
163
149
  console.error("Failed to record usage:", error);
164
150
  }
165
151
  }
166
152
 
167
- export function getUsageStats(hours: number = 24): UsageSummary {
153
+ export async function getUsageStats(hours: number = 24): Promise<UsageSummary> {
168
154
  log.info(`[USAGE STATS] Fetching stats for last ${hours} hours`);
169
- const db = getDb();
155
+ const db = await getHiveDB();
156
+ const col = db.collection<UsageRecord>("usage_records");
170
157
  const since = Math.floor(Date.now() / 1000) - (hours * 3600);
171
158
 
172
- const totals = db.prepare(`
173
- SELECT
174
- COALESCE(SUM(input_tokens), 0) as total_input,
175
- COALESCE(SUM(output_tokens), 0) as total_output,
176
- COALESCE(SUM(cost_usd), 0) as total_cost,
177
- COALESCE(SUM(toon_saved_tokens), 0) as toon_saved_tokens,
178
- COALESCE(SUM(toon_saved_cost), 0) as toon_saved_cost,
179
- COALESCE(SUM(toon_saved_bytes), 0) as toon_saved_bytes,
180
- COALESCE(SUM(toon_saved_percent), 0) as toon_saved_percent,
181
- COALESCE(SUM(toon_json_tokens), 0) as toon_json_tokens,
182
- COALESCE(SUM(toon_toon_tokens), 0) as toon_toon_tokens
183
- FROM usage_records
184
- WHERE created_at >= ?
185
- `).get(since) as {
186
- total_input: number;
187
- total_output: number;
188
- total_cost: number;
189
- toon_saved_tokens: number;
190
- toon_saved_cost: number;
191
- toon_saved_bytes: number;
192
- toon_saved_percent: number;
193
- toon_json_tokens: number;
194
- toon_toon_tokens: number;
195
- };
196
-
197
- const byProvider = db.prepare(`
198
- SELECT
199
- provider,
200
- COALESCE(SUM(input_tokens), 0) as input_tokens,
201
- COALESCE(SUM(output_tokens), 0) as output_tokens,
202
- COALESCE(SUM(cost_usd), 0) as cost_usd
203
- FROM usage_records
204
- WHERE created_at >= ? AND provider != 'toon'
205
- GROUP BY provider
206
- `).all(since) as Array<{ provider: string; input_tokens: number; output_tokens: number; cost_usd: number }>;
159
+ const entries = await col.scan();
160
+ const records = entries.map(e => e.doc).filter(r => r.created_at >= since);
161
+
162
+ const totals = records.reduce((acc, r) => ({
163
+ total_input: acc.total_input + r.input_tokens,
164
+ total_output: acc.total_output + r.output_tokens,
165
+ total_cost: acc.total_cost + r.cost_usd,
166
+ toon_saved_tokens: acc.toon_saved_tokens + r.toon_saved_tokens,
167
+ toon_saved_cost: acc.toon_saved_cost + r.toon_saved_cost,
168
+ toon_saved_bytes: acc.toon_saved_bytes + r.toon_saved_bytes,
169
+ toon_saved_percent: acc.toon_saved_percent + r.toon_saved_percent,
170
+ toon_json_tokens: acc.toon_json_tokens + r.toon_json_tokens,
171
+ toon_toon_tokens: acc.toon_toon_tokens + r.toon_toon_tokens,
172
+ }), {
173
+ total_input: 0,
174
+ total_output: 0,
175
+ total_cost: 0,
176
+ toon_saved_tokens: 0,
177
+ toon_saved_cost: 0,
178
+ toon_saved_bytes: 0,
179
+ toon_saved_percent: 0,
180
+ toon_json_tokens: 0,
181
+ toon_toon_tokens: 0,
182
+ });
207
183
 
208
- const byModel = db.prepare(`
209
- SELECT
210
- model,
211
- provider,
212
- COALESCE(SUM(input_tokens), 0) as input_tokens,
213
- COALESCE(SUM(output_tokens), 0) as output_tokens,
214
- COALESCE(SUM(cost_usd), 0) as cost_usd
215
- FROM usage_records
216
- WHERE created_at >= ? AND provider != 'toon'
217
- GROUP BY model
218
- ORDER BY cost_usd DESC
219
- `).all(since) as Array<{ model: string; provider: string; input_tokens: number; output_tokens: number; cost_usd: number }>;
184
+ const providerMap: UsageSummary["byProvider"] = {};
185
+ const modelMap: UsageSummary["byModel"] = {};
220
186
 
221
- const recentRecords = db.prepare(`
222
- SELECT * FROM usage_records
223
- WHERE created_at >= ?
224
- ORDER BY created_at DESC
225
- LIMIT 20
226
- `).all(since) as UsageRecord[];
187
+ for (const r of records) {
188
+ if (r.provider === "toon") continue;
189
+ if (!providerMap[r.provider]) {
190
+ providerMap[r.provider] = { inputTokens: 0, outputTokens: 0, tokens: 0, costUsd: 0 };
191
+ }
192
+ providerMap[r.provider].inputTokens += r.input_tokens;
193
+ providerMap[r.provider].outputTokens += r.output_tokens;
194
+ providerMap[r.provider].tokens += r.input_tokens + r.output_tokens;
195
+ providerMap[r.provider].costUsd += r.cost_usd;
227
196
 
228
- const providerMap: UsageSummary["byProvider"] = {};
229
- for (const p of byProvider) {
230
- providerMap[p.provider] = {
231
- inputTokens: p.input_tokens,
232
- outputTokens: p.output_tokens,
233
- tokens: p.input_tokens + p.output_tokens,
234
- costUsd: p.cost_usd
235
- };
197
+ if (!modelMap[r.model]) {
198
+ modelMap[r.model] = { provider: r.provider, inputTokens: 0, outputTokens: 0, tokens: 0, costUsd: 0 };
199
+ }
200
+ modelMap[r.model].inputTokens += r.input_tokens;
201
+ modelMap[r.model].outputTokens += r.output_tokens;
202
+ modelMap[r.model].tokens += r.input_tokens + r.output_tokens;
203
+ modelMap[r.model].costUsd += r.cost_usd;
236
204
  }
237
205
 
238
- const modelMap: UsageSummary["byModel"] = {};
239
- for (const m of byModel) {
240
- modelMap[m.model] = {
241
- provider: m.provider,
242
- inputTokens: m.input_tokens,
243
- outputTokens: m.output_tokens,
244
- tokens: m.input_tokens + m.output_tokens,
245
- costUsd: m.cost_usd
246
- };
247
- }
206
+ const recentRecords = records
207
+ .filter(r => r.created_at >= since)
208
+ .sort((a, b) => b.created_at - a.created_at)
209
+ .slice(0, 20);
248
210
 
249
211
  const totalTokens = totals.total_input + totals.total_output;
250
212
  const totalIncludingSaved = totalTokens + totals.toon_saved_tokens;
@@ -252,7 +214,6 @@ export function getUsageStats(hours: number = 24): UsageSummary {
252
214
  ? (totals.toon_saved_tokens / totalIncludingSaved) * 100
253
215
  : 0;
254
216
 
255
- // Calculate average bytes savings percent
256
217
  const toonSavedBytesPercent = totals.toon_toon_tokens > 0
257
218
  ? (totals.toon_saved_bytes / totals.toon_toon_tokens) * 100
258
219
  : 0;
@@ -271,7 +232,7 @@ export function getUsageStats(hours: number = 24): UsageSummary {
271
232
  toonSavingsPercent,
272
233
  byProvider: providerMap,
273
234
  byModel: modelMap,
274
- recentRecords
235
+ recentRecords,
275
236
  };
276
237
  }
277
238
 
@@ -284,14 +245,9 @@ export function estimateCostForTokens(model: string, tokens: number): number {
284
245
  return (tokens / 1_000_000) * pricing.inputPer1M;
285
246
  }
286
247
 
287
- /**
288
- * Get average cost per token for a model (input + output average)
289
- */
290
248
  export function getAverageTokenCost(model: string): number {
291
- // 1. Exact match
292
249
  let pricing = MODEL_PRICING[model];
293
250
 
294
- // 2. Try stripping a single provider prefix (e.g. "openrouter/moonshotai/kimi" → "moonshotai/kimi")
295
251
  if (!pricing) {
296
252
  const slashIdx = model.indexOf('/');
297
253
  if (slashIdx !== -1) {
@@ -299,7 +255,6 @@ export function getAverageTokenCost(model: string): number {
299
255
  }
300
256
  }
301
257
 
302
- // 3. Partial match — find any key whose name is contained in the model string
303
258
  if (!pricing) {
304
259
  for (const [key, p] of Object.entries(MODEL_PRICING)) {
305
260
  if (model.includes(key) || key.includes(model)) {
@@ -310,15 +265,10 @@ export function getAverageTokenCost(model: string): number {
310
265
  }
311
266
 
312
267
  if (!pricing) return 0;
313
- // Average of input and output cost per token
314
268
  return (pricing.inputPer1M + pricing.outputPer1M) / 2 / 1_000_000;
315
269
  }
316
270
 
317
- /**
318
- * Record TOON savings for metrics tracking
319
- * This updates the usage_records table with complete TOON compression metrics
320
- */
321
- export function recordToonSavings(
271
+ export async function recordToonSavings(
322
272
  analysis: {
323
273
  jsonBytes: number;
324
274
  toonBytes: number;
@@ -329,46 +279,35 @@ export function recordToonSavings(
329
279
  savedTokens: number;
330
280
  savedTokensPercent: number;
331
281
  },
332
- costSaved: number,
282
+ costSaved: number,
333
283
  category: string
334
- ): void {
335
- // Fire-and-forget to avoid blocking
336
- Promise.resolve().then(async () => {
337
- try {
338
- const db = getDb();
339
-
340
- // Insert TOON savings record with complete metrics
341
- db.query(`
342
- INSERT INTO usage_records (
343
- id, provider, model, input_tokens, output_tokens, cost_usd,
344
- toon_saved_tokens, toon_saved_cost,
345
- toon_json_bytes, toon_toon_bytes, toon_saved_bytes, toon_saved_percent,
346
- toon_json_tokens, toon_toon_tokens, toon_saved_tokens_pct,
347
- created_at
348
- )
349
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
350
- `).run(
351
- `toon_${category}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
352
- 'toon',
353
- category,
354
- 0,
355
- 0,
356
- 0,
357
- Math.max(0, analysis.savedTokens),
358
- costSaved,
359
- analysis.jsonBytes,
360
- analysis.toonBytes,
361
- analysis.savedBytes,
362
- Math.max(0, analysis.savedPercent),
363
- analysis.jsonTokens,
364
- analysis.toonTokens,
365
- Math.max(0, analysis.savedTokensPercent),
366
- Math.floor(Date.now() / 1000),
367
- )
368
-
369
- log.debug(`[TOON] Recorded ${analysis.savedTokens} tokens ($${costSaved.toFixed(6)}) saved for ${category}`)
370
- } catch (error) {
371
- log.warn(`[TOON] Failed to record savings:`, error)
372
- }
373
- })
284
+ ): Promise<void> {
285
+ try {
286
+ const db = await getHiveDB();
287
+ const col = db.collection<UsageRecord>("usage_records");
288
+
289
+ await col.put(`toon_${category}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, {
290
+ id: `toon_${category}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
291
+ provider: "toon",
292
+ model: category,
293
+ input_tokens: 0,
294
+ output_tokens: 0,
295
+ cost_usd: 0,
296
+ latency_ms: null,
297
+ toon_saved_tokens: Math.max(0, analysis.savedTokens),
298
+ toon_saved_cost: costSaved,
299
+ toon_json_bytes: analysis.jsonBytes,
300
+ toon_toon_bytes: analysis.toonBytes,
301
+ toon_saved_bytes: analysis.savedBytes,
302
+ toon_saved_percent: Math.max(0, analysis.savedPercent),
303
+ toon_json_tokens: analysis.jsonTokens,
304
+ toon_toon_tokens: analysis.toonTokens,
305
+ toon_saved_tokens_pct: Math.max(0, analysis.savedTokensPercent),
306
+ created_at: Math.floor(Date.now() / 1000),
307
+ });
308
+
309
+ log.debug(`[TOON] Recorded ${analysis.savedTokens} tokens ($${costSaved.toFixed(6)}) saved for ${category}`);
310
+ } catch (error) {
311
+ log.warn(`[TOON] Failed to record savings:`, error);
312
+ }
374
313
  }
@@ -20,10 +20,18 @@ describe("tool runtime worker pool", () => {
20
20
  });
21
21
 
22
22
  it("runs multiple tools in parallel through worker scheduling", async () => {
23
+ // Per-tool delay + threshold sized for CI headroom: worker spawn/scheduling
24
+ // overhead is roughly fixed, so a larger delay keeps that overhead a small
25
+ // fraction of the budget instead of dominating it on a loaded runner.
26
+ // Serial execution would take ~3x the delay; the threshold stays well
27
+ // under that so the assertion still proves parallelism, not just patience.
28
+ const TOOL_DELAY_MS = 200;
29
+ const PARALLEL_THRESHOLD_MS = 450;
30
+
23
31
  const tools: RuntimeTool[] = ["slow_a", "slow_b", "slow_c"].map((name) => ({
24
32
  name,
25
33
  execute: async () => {
26
- await delay(120);
34
+ await delay(TOOL_DELAY_MS);
27
35
  return { name };
28
36
  },
29
37
  }));
@@ -38,12 +46,12 @@ describe("tool runtime worker pool", () => {
38
46
  allTools: tools,
39
47
  toolConfig: {},
40
48
  hiveConfig: loadConfig(),
41
- workerPool: { enabled: true, maxWorkers: 3, toolTimeoutMs: 1000, parallelToolCalls: true },
49
+ workerPool: { enabled: true, maxWorkers: 3, toolTimeoutMs: 5000, parallelToolCalls: true },
42
50
  });
43
51
  const elapsed = performance.now() - startedAt;
44
52
 
45
53
  expect(results.map((result) => (result.result as any).name)).toEqual(["slow_a", "slow_b", "slow_c"]);
46
- expect(elapsed).toBeLessThan(260);
54
+ expect(elapsed).toBeLessThan(PARALLEL_THRESHOLD_MS);
47
55
  });
48
56
 
49
57
  it("preserves input order when tools complete out of order", async () => {
@@ -8,7 +8,8 @@
8
8
  */
9
9
 
10
10
  import type { Tool } from "../types.ts";
11
- import { getDb } from "../../storage/SQLiteStorage.ts";
11
+ import { getHiveDB } from "../../storage/HiveDBStorage.ts";
12
+ import type { HiveProviderDoc, HiveModelDoc } from "../../storage/hiveSeed.ts";
12
13
 
13
14
  export const getAvailableModelsTool: Tool = {
14
15
  name: "get_available_models",
@@ -31,7 +32,6 @@ export const getAvailableModelsTool: Tool = {
31
32
  },
32
33
  },
33
34
  execute: async (params: Record<string, unknown>) => {
34
- const db = getDb();
35
35
  const { providerId, modelType, capabilities } = params as {
36
36
  providerId?: string;
37
37
  modelType?: string;
@@ -39,74 +39,70 @@ export const getAvailableModelsTool: Tool = {
39
39
  };
40
40
 
41
41
  try {
42
- // Construir query con filtros opcionales
43
- let query = `
44
- SELECT
45
- p.id as provider_id,
46
- p.name as provider_name,
47
- p.category as provider_category,
48
- m.id as model_id,
49
- m.name as model_name,
50
- m.model_type,
51
- m.context_window,
52
- m.capabilities
53
- FROM models m
54
- INNER JOIN providers p ON m.provider_id = p.id
55
- WHERE m.enabled = 1 AND m.active = 1 AND p.enabled = 1 AND p.active = 1
56
- `;
42
+ const db = await getHiveDB();
43
+ const providersCol = db.collection<HiveProviderDoc>("providers");
44
+ const modelsCol = db.collection<HiveModelDoc>("models");
57
45
 
58
- const whereClauses: string[] = [];
59
- const queryParams: string[] = [];
46
+ const [providers, models] = await Promise.all([
47
+ providersCol.scan(),
48
+ modelsCol.scan(),
49
+ ]);
50
+
51
+ const activeProviders = new Map(
52
+ providers
53
+ .filter(p => p.doc.enabled && p.doc.active)
54
+ .map(p => [p.id, p.doc])
55
+ );
56
+
57
+ let rows = models
58
+ .filter(m => m.doc.enabled && m.doc.active)
59
+ .map(m => {
60
+ const provider = activeProviders.get(m.doc.providerId);
61
+ if (!provider) return null;
62
+ return {
63
+ providerId: provider.id,
64
+ providerName: provider.name,
65
+ providerCategory: provider.category,
66
+ modelId: m.doc.id,
67
+ modelName: m.doc.name,
68
+ modelType: m.doc.modelType,
69
+ contextWindow: m.doc.contextWindow ?? null,
70
+ capabilities: m.doc.capabilities ?? null,
71
+ };
72
+ })
73
+ .filter(Boolean) as Array<{
74
+ providerId: string;
75
+ providerName: string;
76
+ providerCategory: string;
77
+ modelId: string;
78
+ modelName: string;
79
+ modelType: string;
80
+ contextWindow: number | null;
81
+ capabilities: string[] | null;
82
+ }>;
60
83
 
61
84
  if (providerId) {
62
- whereClauses.push("p.id = ?");
63
- queryParams.push(providerId as string);
85
+ rows = rows.filter(r => r.providerId === providerId);
64
86
  }
65
87
 
66
88
  if (modelType) {
67
- whereClauses.push("m.model_type = ?");
68
- queryParams.push(modelType as string);
89
+ rows = rows.filter(r => r.modelType === modelType);
69
90
  }
70
91
 
71
92
  if (capabilities) {
72
- whereClauses.push("m.capabilities LIKE ?");
73
- queryParams.push(`%${capabilities as string}%`);
74
- }
75
-
76
- if (whereClauses.length > 0) {
77
- query += " AND " + whereClauses.join(" AND ");
93
+ const cap = capabilities.toLowerCase();
94
+ rows = rows.filter(r => r.capabilities?.some(c => c.toLowerCase().includes(cap)));
78
95
  }
79
96
 
80
- query += " ORDER BY p.name, m.name";
81
-
82
- // Ejecutar query
83
- const rows = db.query<any, string[]>(query).all(...queryParams) as Array<{
84
- provider_id: string;
85
- provider_name: string;
86
- provider_category: string;
87
- model_id: string;
88
- model_name: string;
89
- model_type: string;
90
- context_window: number | null;
91
- capabilities: string | null;
92
- }>;
93
-
94
- // Transformar a formato amigable
95
- const result = rows.map(row => ({
96
- providerId: row.provider_id,
97
- providerName: row.provider_name,
98
- providerCategory: row.provider_category,
99
- modelId: row.model_id,
100
- modelName: row.model_name,
101
- modelType: row.model_type,
102
- contextWindow: row.context_window,
103
- capabilities: row.capabilities ? JSON.parse(row.capabilities) : null,
104
- }));
97
+ rows.sort((a, b) => {
98
+ if (a.providerName !== b.providerName) return a.providerName.localeCompare(b.providerName);
99
+ return a.modelName.localeCompare(b.modelName);
100
+ });
105
101
 
106
102
  return {
107
103
  ok: true,
108
- count: result.length,
109
- models: result,
104
+ count: rows.length,
105
+ models: rows,
110
106
  };
111
107
  } catch (error) {
112
108
  return {