@johpaz/hive-sdk 0.0.15 → 0.0.16

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 (94) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +179 -57
  3. package/docs/API-AGENTS.md +9 -9
  4. package/docs/API-CONTEXT-COMPILER.md +13 -13
  5. package/docs/API-DAG-SCHEDULER.md +8 -8
  6. package/docs/API-TOOLS-SKILLS-CHANNELS.md +150 -93
  7. package/docs/API-WORKERS-EVENTS.md +206 -59
  8. package/docs/INDEX.md +99 -50
  9. package/docs/README.md +117 -24
  10. package/docs/TEMPLATE-HIVE-APP.md +360 -0
  11. package/package.json +13 -6
  12. package/packages/cli/bin/hive +2 -0
  13. package/packages/cli/src/commands/add-skill.ts +42 -0
  14. package/packages/cli/src/commands/add-tool.ts +45 -0
  15. package/packages/cli/src/commands/add-worker.ts +49 -0
  16. package/packages/cli/src/commands/create-app-utils.ts +32 -0
  17. package/packages/cli/src/commands/create-app.test.ts +151 -0
  18. package/packages/cli/src/commands/create-app.ts +35 -0
  19. package/packages/cli/src/index.ts +21 -5
  20. package/packages/cli/templates/hive-app/.env.example +17 -0
  21. package/packages/cli/templates/hive-app/docker-compose.yml +20 -0
  22. package/packages/cli/templates/hive-app/hive.config.ts +19 -0
  23. package/packages/cli/templates/hive-app/package.json +16 -0
  24. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +9 -0
  25. package/packages/cli/templates/hive-app/src/main.ts +56 -0
  26. package/packages/core/src/auth/auth.ts +108 -0
  27. package/packages/core/src/auth/index.ts +1 -0
  28. package/packages/core/src/canvas/canvas.test.ts +32 -0
  29. package/packages/core/src/canvas/emitter.ts +1 -1
  30. package/packages/core/src/canvas/index.ts +3 -6
  31. package/packages/core/src/channels/base.ts +154 -0
  32. package/packages/core/src/channels/channels.test.ts +18 -0
  33. package/packages/core/src/channels/discord.ts +273 -0
  34. package/packages/core/src/channels/index.ts +7 -0
  35. package/packages/core/src/channels/manager.ts +450 -0
  36. package/packages/core/src/channels/slack.ts +323 -0
  37. package/packages/core/src/channels/telegram.ts +612 -0
  38. package/packages/core/src/channels/webchat.ts +139 -0
  39. package/packages/core/src/channels/whatsapp.ts +548 -0
  40. package/packages/core/src/events/agent-bus.ts +460 -0
  41. package/packages/core/src/events/event-bus.ts +169 -0
  42. package/packages/core/src/gateway/channel-notify.ts +32 -7
  43. package/packages/core/src/gateway/gateway.test.ts +38 -0
  44. package/packages/core/src/gateway/index.ts +2 -1
  45. package/packages/core/src/gateway/server.ts +139 -0
  46. package/packages/core/src/heartbeat/index.ts +157 -0
  47. package/packages/core/src/index.ts +44 -0
  48. package/packages/core/src/multimodal/index.ts +2 -2
  49. package/packages/core/src/multimodal/vision-service.ts +283 -0
  50. package/packages/core/src/plugins/api.ts +128 -0
  51. package/packages/core/src/plugins/index.ts +2 -0
  52. package/packages/core/src/plugins/loader.ts +365 -0
  53. package/packages/core/src/resilience/circuit-breaker.ts +225 -0
  54. package/packages/core/src/scheduler/CronScheduler.ts +699 -0
  55. package/packages/core/src/scheduler/dag/AgentExecutor.ts +53 -0
  56. package/packages/core/src/scheduler/dag/DAGScheduler.ts +250 -0
  57. package/packages/core/src/scheduler/dag/EventBridge.ts +122 -0
  58. package/packages/core/src/scheduler/dag/TaskGraph.ts +192 -0
  59. package/packages/core/src/scheduler/dag/TaskNode.ts +97 -0
  60. package/packages/core/src/scheduler/dag/TaskResult.ts +22 -0
  61. package/packages/core/src/scheduler/dag/errors.ts +37 -0
  62. package/packages/core/src/scheduler/dag/index.ts +26 -0
  63. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +97 -0
  64. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +21 -0
  65. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +46 -0
  66. package/packages/core/src/scheduler/index.ts +22 -0
  67. package/packages/core/src/scheduler/integration.ts +237 -0
  68. package/packages/core/src/scheduler/scheduler.test.ts +19 -0
  69. package/packages/core/src/scheduler/types.ts +164 -0
  70. package/packages/core/src/security/google-chat.ts +269 -0
  71. package/packages/core/src/security/index.ts +192 -4
  72. package/packages/core/src/security/rate-limit.ts +270 -0
  73. package/packages/core/src/security/signal.ts +321 -0
  74. package/packages/core/src/storage/crypto.ts +198 -66
  75. package/packages/core/src/storage/storage.test.ts +37 -0
  76. package/packages/core/src/swarm/swarm.test.ts +24 -0
  77. package/packages/core/src/tool-runtime/index.ts +522 -0
  78. package/packages/core/src/tool-runtime/tool-runtime.test.ts +91 -0
  79. package/packages/core/src/tool-runtime/tool-worker.ts +125 -0
  80. package/packages/core/src/voice/index.ts +5 -18
  81. package/packages/core/src/workers/WorkerPool.ts +167 -0
  82. package/packages/core/src/workers/agent.worker.ts +68 -0
  83. package/packages/core/src/workers/createWorker.ts +144 -0
  84. package/packages/core/src/workers/index.ts +5 -0
  85. package/packages/core/src/workers/workers.test.ts +48 -0
  86. package/test/setup-db.ts +2 -2
  87. package/tsconfig.json +2 -1
  88. package/.github/CODEOWNERS +0 -9
  89. package/.github/workflows/publish.yml +0 -89
  90. package/.github/workflows/version-bump.yml +0 -102
  91. package/bun.lock +0 -543
  92. package/bunfig.toml +0 -7
  93. package/packages/core/src/agent/providers.ts +0 -1
  94. package/packages/core/src/gateway/channel-notify.test.ts +0 -14
@@ -0,0 +1,522 @@
1
+ import { existsSync } from "node:fs"
2
+ import { fileURLToPath } from "node:url"
3
+ import { availableParallelism } from "node:os"
4
+ import type { Config } from "../config/loader.ts"
5
+ import { loadConfig } from "../config/loader.ts"
6
+
7
+ export type ToolCallLike = {
8
+ id: string
9
+ function: {
10
+ name: string
11
+ arguments: unknown
12
+ }
13
+ }
14
+
15
+ export type RuntimeTool = {
16
+ name: string
17
+ execute?: (params: Record<string, unknown>, config?: any) => Promise<unknown>
18
+ }
19
+
20
+ export type ToolRuntimeConfig = {
21
+ enabled?: boolean
22
+ maxWorkers?: number
23
+ toolTimeoutMs?: number
24
+ parallelToolCalls?: boolean
25
+ }
26
+
27
+ export type ExecuteToolBatchOptions = {
28
+ toolCalls: ToolCallLike[]
29
+ allTools: RuntimeTool[]
30
+ toolConfig: {
31
+ user_id?: string
32
+ thread_id?: string
33
+ channel?: string
34
+ workspace?: string | null
35
+ }
36
+ hiveConfig?: Config
37
+ workerPool?: ToolRuntimeConfig
38
+ mainThreadToolNames?: string[]
39
+ signal?: AbortSignal
40
+ }
41
+
42
+ export type ToolBatchResult = {
43
+ toolCall: ToolCallLike
44
+ toolName: string
45
+ result: unknown
46
+ ok: boolean
47
+ durationMs: number
48
+ error?: SerializedError
49
+ timedOut?: boolean
50
+ aborted?: boolean
51
+ }
52
+
53
+ type SerializedError = {
54
+ name: string
55
+ message: string
56
+ stack?: string
57
+ }
58
+
59
+ type WorkerMessage =
60
+ | {
61
+ type: "result"
62
+ jobId: string
63
+ ok: boolean
64
+ result?: unknown
65
+ error?: SerializedError
66
+ durationMs: number
67
+ }
68
+ | {
69
+ type: "rpc_call"
70
+ rpcId: string
71
+ jobId: string
72
+ toolName: string
73
+ args: unknown
74
+ toolConfig: Record<string, unknown>
75
+ }
76
+
77
+ type QueuedJob = {
78
+ id: string
79
+ batchId: string
80
+ index: number
81
+ toolCall: ToolCallLike
82
+ allTools: RuntimeTool[]
83
+ toolConfig: ExecuteToolBatchOptions["toolConfig"]
84
+ hiveConfig: Config
85
+ mainThreadToolNames: string[]
86
+ timeoutMs: number
87
+ resolve: (result: ToolBatchResult) => void
88
+ settled: boolean
89
+ startedAt: number
90
+ timer?: ReturnType<typeof setTimeout>
91
+ }
92
+
93
+ type WorkerSlot = {
94
+ worker: Worker
95
+ busy: boolean
96
+ job?: QueuedJob
97
+ }
98
+
99
+ function resolveWorkerEntry(): string {
100
+ const candidates = [
101
+ new URL("./tool-worker.js", import.meta.url),
102
+ new URL("./tool-worker.ts", import.meta.url),
103
+ new URL("../packages/core/src/tool-runtime/tool-worker.js", import.meta.url),
104
+ new URL("../packages/core/src/tool-runtime/tool-worker.ts", import.meta.url),
105
+ ]
106
+
107
+ for (const candidate of candidates) {
108
+ if (existsSync(fileURLToPath(candidate))) {
109
+ return candidate.href
110
+ }
111
+ }
112
+
113
+ throw new Error(
114
+ `Tool worker entry not found. Tried: ${candidates.map((candidate) => fileURLToPath(candidate)).join(", ")}`
115
+ )
116
+ }
117
+
118
+ function serializeError(error: unknown): SerializedError {
119
+ const err = error instanceof Error ? error : new Error(String(error))
120
+ return {
121
+ name: err.name,
122
+ message: err.message,
123
+ stack: err.stack,
124
+ }
125
+ }
126
+
127
+ function toolErrorResult(
128
+ toolName: string,
129
+ message: string,
130
+ extra: Partial<ToolBatchResult> = {}
131
+ ): unknown {
132
+ return {
133
+ error: true,
134
+ tool: toolName,
135
+ message,
136
+ timestamp: new Date().toISOString(),
137
+ ...extra,
138
+ }
139
+ }
140
+
141
+ const DEFAULT_MAIN_THREAD_TOOL_NAMES = new Set([
142
+ // These tools depend on process-local singleton state (SQLite DB, live
143
+ // channel senders, schedulers, browser sessions, or in-memory services).
144
+ "search_knowledge",
145
+ "save_note",
146
+ "memory_write",
147
+ "memory_read",
148
+ "memory_list",
149
+ "memory_search",
150
+ "memory_delete",
151
+ "agent_create",
152
+ "agent_find",
153
+ "agent_archive",
154
+ "task_status",
155
+ "bus_publish",
156
+ "bus_read",
157
+ "get_available_models",
158
+ "meeting_start",
159
+ "meeting_add_segment",
160
+ "meeting_stop",
161
+ "meeting_report",
162
+ "browser_navigate",
163
+ "browser_screenshot",
164
+ "browser_click",
165
+ "browser_type",
166
+ "browser_extract",
167
+ "browser_script",
168
+ "browser_wait",
169
+ "canvas_render",
170
+ "canvas_ask",
171
+ "canvas_confirm",
172
+ "canvas_show_card",
173
+ "canvas_show_progress",
174
+ "canvas_show_list",
175
+ "canvas_clear",
176
+ "a2ui_create_surface",
177
+ "a2ui_update_components",
178
+ "a2ui_update_data_model",
179
+ "a2ui_delete_surface",
180
+ "cron.create",
181
+ "cron.list",
182
+ "cron.update",
183
+ "cron.pause",
184
+ "cron.resume",
185
+ "cron.delete",
186
+ "cron.trigger",
187
+ "cron.history",
188
+ "notify",
189
+ "report_progress",
190
+ "task_delegate",
191
+ "task_delegate_code",
192
+ "voice_transcribe",
193
+ "voice_speak",
194
+ ])
195
+
196
+ async function executeInMainThread(job: {
197
+ toolCall: ToolCallLike
198
+ allTools: RuntimeTool[]
199
+ toolConfig: ExecuteToolBatchOptions["toolConfig"]
200
+ }): Promise<unknown> {
201
+ const toolName = job.toolCall.function.name
202
+ const tool = job.allTools.find((candidate) => candidate.name === toolName)
203
+ if (!tool?.execute) {
204
+ return toolErrorResult(toolName, `Tool '${toolName}' not found or not executable`)
205
+ }
206
+
207
+ try {
208
+ const args = typeof job.toolCall.function.arguments === "string"
209
+ ? JSON.parse(job.toolCall.function.arguments)
210
+ : job.toolCall.function.arguments
211
+ return await tool.execute((args ?? {}) as Record<string, unknown>, { configurable: job.toolConfig })
212
+ } catch (error) {
213
+ return toolErrorResult(toolName, (error as Error).message)
214
+ }
215
+ }
216
+
217
+ class ToolWorkerPool {
218
+ private workers: WorkerSlot[] = []
219
+ private queue: QueuedJob[] = []
220
+ private readonly maxWorkers: number
221
+
222
+ constructor(maxWorkers: number) {
223
+ this.maxWorkers = Math.max(1, maxWorkers)
224
+ }
225
+
226
+ execute(job: Omit<QueuedJob, "resolve" | "settled" | "startedAt">): Promise<ToolBatchResult> {
227
+ return new Promise((resolve) => {
228
+ this.queue.push({
229
+ ...job,
230
+ resolve,
231
+ settled: false,
232
+ startedAt: 0,
233
+ })
234
+ this.drain()
235
+ })
236
+ }
237
+
238
+ abortBatch(batchId: string, reason = "Tool execution aborted"): void {
239
+ const remaining: QueuedJob[] = []
240
+ const aborted: QueuedJob[] = []
241
+ for (const job of this.queue) {
242
+ if (job.batchId === batchId) aborted.push(job)
243
+ else remaining.push(job)
244
+ }
245
+ this.queue = remaining
246
+
247
+ for (const job of aborted) {
248
+ job.resolve({
249
+ toolCall: job.toolCall,
250
+ toolName: job.toolCall.function.name,
251
+ result: toolErrorResult(job.toolCall.function.name, reason),
252
+ ok: false,
253
+ durationMs: 0,
254
+ error: { name: "AbortError", message: reason },
255
+ aborted: true,
256
+ })
257
+ }
258
+
259
+ for (const slot of this.workers) {
260
+ if (slot.busy && slot.job?.batchId === batchId) {
261
+ this.finishJob(slot, {
262
+ toolCall: slot.job.toolCall,
263
+ toolName: slot.job.toolCall.function.name,
264
+ result: toolErrorResult(slot.job.toolCall.function.name, reason),
265
+ ok: false,
266
+ durationMs: Math.round(performance.now() - slot.job.startedAt),
267
+ error: { name: "AbortError", message: reason },
268
+ aborted: true,
269
+ }, true)
270
+ }
271
+ }
272
+ }
273
+
274
+ dispose(): void {
275
+ for (const slot of this.workers) {
276
+ slot.worker.terminate()
277
+ }
278
+ this.workers = []
279
+ this.queue = []
280
+ }
281
+
282
+ private drain(): void {
283
+ while (this.queue.length > 0) {
284
+ const slot = this.getIdleSlot()
285
+ if (!slot) return
286
+
287
+ const job = this.queue.shift()!
288
+ this.startJob(slot, job)
289
+ }
290
+ }
291
+
292
+ private getIdleSlot(): WorkerSlot | null {
293
+ const idle = this.workers.find((slot) => !slot.busy)
294
+ if (idle) return idle
295
+
296
+ if (this.workers.length >= this.maxWorkers) return null
297
+
298
+ const slot = this.createSlot()
299
+ this.workers.push(slot)
300
+ return slot
301
+ }
302
+
303
+ private createSlot(): WorkerSlot {
304
+ const worker = new Worker(resolveWorkerEntry(), { type: "module" })
305
+ const slot: WorkerSlot = { worker, busy: false }
306
+
307
+ worker.onmessage = (event: MessageEvent<WorkerMessage>) => {
308
+ const message = event.data
309
+ if (message.type === "rpc_call") {
310
+ void this.handleRpc(slot, message)
311
+ return
312
+ }
313
+
314
+ const job = slot.job
315
+ if (!job || job.id !== message.jobId) return
316
+
317
+ this.finishJob(slot, {
318
+ toolCall: job.toolCall,
319
+ toolName: job.toolCall.function.name,
320
+ result: message.ok ? message.result : toolErrorResult(job.toolCall.function.name, message.error?.message || "Tool failed"),
321
+ ok: message.ok && !(message.result && typeof message.result === "object" && (message.result as any).error === true),
322
+ durationMs: message.durationMs,
323
+ error: message.error,
324
+ })
325
+ }
326
+
327
+ worker.onerror = (event) => {
328
+ const job = slot.job
329
+ if (!job) return
330
+
331
+ const location = [event.filename, event.lineno, event.colno].filter(Boolean).join(":")
332
+ const message = location
333
+ ? `${event.message || "Tool worker failed"} (${location})`
334
+ : (event.message || "Tool worker failed")
335
+
336
+ this.finishJob(slot, {
337
+ toolCall: job.toolCall,
338
+ toolName: job.toolCall.function.name,
339
+ result: toolErrorResult(job.toolCall.function.name, message),
340
+ ok: false,
341
+ durationMs: Math.round(performance.now() - job.startedAt),
342
+ error: { name: "WorkerError", message },
343
+ }, true)
344
+ }
345
+
346
+ return slot
347
+ }
348
+
349
+ private startJob(slot: WorkerSlot, job: QueuedJob): void {
350
+ slot.busy = true
351
+ slot.job = job
352
+ job.startedAt = performance.now()
353
+ job.timer = setTimeout(() => {
354
+ if (job.settled) return
355
+
356
+ this.finishJob(slot, {
357
+ toolCall: job.toolCall,
358
+ toolName: job.toolCall.function.name,
359
+ result: toolErrorResult(job.toolCall.function.name, `Tool timed out after ${job.timeoutMs}ms`),
360
+ ok: false,
361
+ durationMs: Math.round(performance.now() - job.startedAt),
362
+ error: { name: "TimeoutError", message: `Tool timed out after ${job.timeoutMs}ms` },
363
+ timedOut: true,
364
+ }, true)
365
+ }, job.timeoutMs)
366
+
367
+ slot.worker.postMessage({
368
+ type: "run",
369
+ jobId: job.id,
370
+ toolName: job.toolCall.function.name,
371
+ args: job.toolCall.function.arguments,
372
+ toolConfig: job.toolConfig,
373
+ hiveConfig: job.hiveConfig,
374
+ mainThreadToolNames: job.mainThreadToolNames,
375
+ })
376
+ }
377
+
378
+ private async handleRpc(
379
+ slot: WorkerSlot,
380
+ message: Extract<WorkerMessage, { type: "rpc_call" }>
381
+ ): Promise<void> {
382
+ const job = slot.job
383
+ if (!job || job.id !== message.jobId || job.settled) return
384
+
385
+ try {
386
+ const result = await executeInMainThread({
387
+ toolCall: {
388
+ id: job.toolCall.id,
389
+ function: {
390
+ name: message.toolName,
391
+ arguments: message.args,
392
+ },
393
+ },
394
+ allTools: job.allTools,
395
+ toolConfig: job.toolConfig,
396
+ })
397
+ slot.worker.postMessage({ type: "rpc_result", rpcId: message.rpcId, ok: true, result })
398
+ } catch (error) {
399
+ slot.worker.postMessage({ type: "rpc_result", rpcId: message.rpcId, ok: false, error: serializeError(error) })
400
+ }
401
+ }
402
+
403
+ private finishJob(slot: WorkerSlot, result: ToolBatchResult, restart = false): void {
404
+ const job = slot.job
405
+ if (!job || job.settled) return
406
+
407
+ job.settled = true
408
+ if (job.timer) clearTimeout(job.timer)
409
+ job.resolve(result)
410
+
411
+ if (restart) {
412
+ slot.worker.terminate()
413
+ const index = this.workers.indexOf(slot)
414
+ if (index >= 0) {
415
+ this.workers[index] = this.createSlot()
416
+ }
417
+ } else {
418
+ slot.busy = false
419
+ slot.job = undefined
420
+ }
421
+
422
+ this.drain()
423
+ }
424
+ }
425
+
426
+ let sharedPool: ToolWorkerPool | null = null
427
+ let sharedPoolSize = 0
428
+
429
+ function getDefaultMaxWorkers(): number {
430
+ return Math.min(4, Math.max(1, availableParallelism()))
431
+ }
432
+
433
+ function resolveRuntimeConfig(config?: ToolRuntimeConfig): Required<ToolRuntimeConfig> {
434
+ return {
435
+ enabled: config?.enabled ?? true,
436
+ maxWorkers: config?.maxWorkers ?? getDefaultMaxWorkers(),
437
+ toolTimeoutMs: config?.toolTimeoutMs ?? 300000,
438
+ parallelToolCalls: config?.parallelToolCalls ?? true,
439
+ }
440
+ }
441
+
442
+ function getPool(maxWorkers: number): ToolWorkerPool {
443
+ if (!sharedPool || sharedPoolSize !== maxWorkers) {
444
+ sharedPool = new ToolWorkerPool(maxWorkers)
445
+ sharedPoolSize = maxWorkers
446
+ }
447
+ return sharedPool
448
+ }
449
+
450
+ export async function executeToolBatch(options: ExecuteToolBatchOptions): Promise<ToolBatchResult[]> {
451
+ const runtimeConfig = resolveRuntimeConfig(options.workerPool)
452
+ const hiveConfig = options.hiveConfig ?? loadConfig()
453
+ const mainThreadToolNames = [
454
+ ...DEFAULT_MAIN_THREAD_TOOL_NAMES,
455
+ ...(options.mainThreadToolNames ?? []),
456
+ ]
457
+
458
+ if (options.signal?.aborted) {
459
+ return options.toolCalls.map((toolCall) => ({
460
+ toolCall,
461
+ toolName: toolCall.function.name,
462
+ result: toolErrorResult(toolCall.function.name, "Tool execution aborted"),
463
+ ok: false,
464
+ durationMs: 0,
465
+ error: { name: "AbortError", message: "Tool execution aborted" },
466
+ aborted: true,
467
+ }))
468
+ }
469
+
470
+ if (!runtimeConfig.enabled || !runtimeConfig.parallelToolCalls || options.toolCalls.length <= 1) {
471
+ const results: ToolBatchResult[] = []
472
+ for (const toolCall of options.toolCalls) {
473
+ const startedAt = performance.now()
474
+ const result = await executeInMainThread({
475
+ toolCall,
476
+ allTools: options.allTools,
477
+ toolConfig: options.toolConfig,
478
+ })
479
+ results.push({
480
+ toolCall,
481
+ toolName: toolCall.function.name,
482
+ result,
483
+ ok: !(result && typeof result === "object" && (result as any).error === true),
484
+ durationMs: Math.round(performance.now() - startedAt),
485
+ })
486
+ }
487
+ return results
488
+ }
489
+
490
+ const pool = getPool(runtimeConfig.maxWorkers)
491
+ const batchId = crypto.randomUUID()
492
+ const abortHandler = () => pool.abortBatch(batchId, "Tool execution aborted")
493
+ options.signal?.addEventListener("abort", abortHandler, { once: true })
494
+
495
+ try {
496
+ const results = await Promise.all(options.toolCalls.map((toolCall, index) => pool.execute({
497
+ id: `${Date.now()}:${index}:${crypto.randomUUID()}`,
498
+ batchId,
499
+ index,
500
+ toolCall,
501
+ allTools: options.allTools,
502
+ toolConfig: options.toolConfig,
503
+ hiveConfig,
504
+ mainThreadToolNames,
505
+ timeoutMs: runtimeConfig.toolTimeoutMs,
506
+ })))
507
+
508
+ return results.sort((a, b) => {
509
+ const aIndex = options.toolCalls.indexOf(a.toolCall)
510
+ const bIndex = options.toolCalls.indexOf(b.toolCall)
511
+ return aIndex - bIndex
512
+ })
513
+ } finally {
514
+ options.signal?.removeEventListener("abort", abortHandler)
515
+ }
516
+ }
517
+
518
+ export function shutdownToolRuntime(): void {
519
+ sharedPool?.dispose()
520
+ sharedPool = null
521
+ sharedPoolSize = 0
522
+ }
@@ -0,0 +1,91 @@
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { executeToolBatch, shutdownToolRuntime, type RuntimeTool, type ToolCallLike } from "./index.ts";
3
+ import { loadConfig } from "../config/loader.ts";
4
+
5
+ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
6
+
7
+ function toolCall(id: string, name: string, args: unknown = {}): ToolCallLike {
8
+ return {
9
+ id,
10
+ function: {
11
+ name,
12
+ arguments: JSON.stringify(args),
13
+ },
14
+ };
15
+ }
16
+
17
+ describe("tool runtime worker pool", () => {
18
+ afterEach(() => {
19
+ shutdownToolRuntime();
20
+ });
21
+
22
+ it("runs multiple tools in parallel through worker scheduling", async () => {
23
+ const tools: RuntimeTool[] = ["slow_a", "slow_b", "slow_c"].map((name) => ({
24
+ name,
25
+ execute: async () => {
26
+ await delay(120);
27
+ return { name };
28
+ },
29
+ }));
30
+
31
+ const startedAt = performance.now();
32
+ const results = await executeToolBatch({
33
+ toolCalls: [
34
+ toolCall("1", "slow_a"),
35
+ toolCall("2", "slow_b"),
36
+ toolCall("3", "slow_c"),
37
+ ],
38
+ allTools: tools,
39
+ toolConfig: {},
40
+ hiveConfig: loadConfig(),
41
+ workerPool: { enabled: true, maxWorkers: 3, toolTimeoutMs: 1000, parallelToolCalls: true },
42
+ });
43
+ const elapsed = performance.now() - startedAt;
44
+
45
+ expect(results.map((result) => (result.result as any).name)).toEqual(["slow_a", "slow_b", "slow_c"]);
46
+ expect(elapsed).toBeLessThan(260);
47
+ });
48
+
49
+ it("preserves input order when tools complete out of order", async () => {
50
+ const tools: RuntimeTool[] = [
51
+ { name: "first", execute: async () => { await delay(120); return { value: 1 } } },
52
+ { name: "second", execute: async () => { await delay(20); return { value: 2 } } },
53
+ { name: "third", execute: async () => { await delay(60); return { value: 3 } } },
54
+ ];
55
+
56
+ const results = await executeToolBatch({
57
+ toolCalls: [toolCall("1", "first"), toolCall("2", "second"), toolCall("3", "third")],
58
+ allTools: tools,
59
+ toolConfig: {},
60
+ hiveConfig: loadConfig(),
61
+ workerPool: { enabled: true, maxWorkers: 3, toolTimeoutMs: 1000, parallelToolCalls: true },
62
+ });
63
+
64
+ expect(results.map((r) => (r.result as any).value)).toEqual([1, 2, 3]);
65
+ });
66
+
67
+ it("handles tool errors gracefully", async () => {
68
+ const tools: RuntimeTool[] = [
69
+ {
70
+ name: "fail",
71
+ execute: async () => {
72
+ throw new Error("intentional failure");
73
+ },
74
+ },
75
+ ];
76
+
77
+ const results = await executeToolBatch({
78
+ toolCalls: [toolCall("1", "fail")],
79
+ allTools: tools,
80
+ toolConfig: {},
81
+ hiveConfig: loadConfig(),
82
+ workerPool: { enabled: true, maxWorkers: 1, toolTimeoutMs: 1000, parallelToolCalls: false },
83
+ });
84
+
85
+ expect(results[0].ok).toBe(false);
86
+ expect(results[0].result).toBeDefined();
87
+ const result = results[0].result as any;
88
+ expect(result.error).toBe(true);
89
+ expect(result.message).toContain("intentional failure");
90
+ });
91
+ });