@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.
- package/CHANGELOG.md +27 -1
- package/README.md +179 -57
- package/docs/API-AGENTS.md +9 -9
- package/docs/API-CONTEXT-COMPILER.md +13 -13
- package/docs/API-DAG-SCHEDULER.md +8 -8
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +150 -93
- package/docs/API-WORKERS-EVENTS.md +206 -59
- package/docs/INDEX.md +99 -50
- package/docs/README.md +117 -24
- package/docs/TEMPLATE-HIVE-APP.md +360 -0
- package/package.json +13 -6
- package/packages/cli/bin/hive +2 -0
- package/packages/cli/src/commands/add-skill.ts +42 -0
- package/packages/cli/src/commands/add-tool.ts +45 -0
- package/packages/cli/src/commands/add-worker.ts +49 -0
- package/packages/cli/src/commands/create-app-utils.ts +32 -0
- package/packages/cli/src/commands/create-app.test.ts +151 -0
- package/packages/cli/src/commands/create-app.ts +35 -0
- package/packages/cli/src/index.ts +21 -5
- package/packages/cli/templates/hive-app/.env.example +17 -0
- package/packages/cli/templates/hive-app/docker-compose.yml +20 -0
- package/packages/cli/templates/hive-app/hive.config.ts +19 -0
- package/packages/cli/templates/hive-app/package.json +16 -0
- package/packages/cli/templates/hive-app/src/agents/coordinator.ts +9 -0
- package/packages/cli/templates/hive-app/src/main.ts +56 -0
- package/packages/core/src/auth/auth.ts +108 -0
- package/packages/core/src/auth/index.ts +1 -0
- package/packages/core/src/canvas/canvas.test.ts +32 -0
- package/packages/core/src/canvas/emitter.ts +1 -1
- package/packages/core/src/canvas/index.ts +3 -6
- package/packages/core/src/channels/base.ts +154 -0
- package/packages/core/src/channels/channels.test.ts +18 -0
- package/packages/core/src/channels/discord.ts +273 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +450 -0
- package/packages/core/src/channels/slack.ts +323 -0
- package/packages/core/src/channels/telegram.ts +612 -0
- package/packages/core/src/channels/webchat.ts +139 -0
- package/packages/core/src/channels/whatsapp.ts +548 -0
- package/packages/core/src/events/agent-bus.ts +460 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/channel-notify.ts +32 -7
- package/packages/core/src/gateway/gateway.test.ts +38 -0
- package/packages/core/src/gateway/index.ts +2 -1
- package/packages/core/src/gateway/server.ts +139 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +44 -0
- package/packages/core/src/multimodal/index.ts +2 -2
- package/packages/core/src/multimodal/vision-service.ts +283 -0
- package/packages/core/src/plugins/api.ts +128 -0
- package/packages/core/src/plugins/index.ts +2 -0
- package/packages/core/src/plugins/loader.ts +365 -0
- package/packages/core/src/resilience/circuit-breaker.ts +225 -0
- package/packages/core/src/scheduler/CronScheduler.ts +699 -0
- package/packages/core/src/scheduler/dag/AgentExecutor.ts +53 -0
- package/packages/core/src/scheduler/dag/DAGScheduler.ts +250 -0
- package/packages/core/src/scheduler/dag/EventBridge.ts +122 -0
- package/packages/core/src/scheduler/dag/TaskGraph.ts +192 -0
- package/packages/core/src/scheduler/dag/TaskNode.ts +97 -0
- package/packages/core/src/scheduler/dag/TaskResult.ts +22 -0
- package/packages/core/src/scheduler/dag/errors.ts +37 -0
- package/packages/core/src/scheduler/dag/index.ts +26 -0
- package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/scheduler/index.ts +22 -0
- package/packages/core/src/scheduler/integration.ts +237 -0
- package/packages/core/src/scheduler/scheduler.test.ts +19 -0
- package/packages/core/src/scheduler/types.ts +164 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -4
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/storage/crypto.ts +198 -66
- package/packages/core/src/storage/storage.test.ts +37 -0
- package/packages/core/src/swarm/swarm.test.ts +24 -0
- package/packages/core/src/tool-runtime/index.ts +522 -0
- package/packages/core/src/tool-runtime/tool-runtime.test.ts +91 -0
- package/packages/core/src/tool-runtime/tool-worker.ts +125 -0
- package/packages/core/src/voice/index.ts +5 -18
- package/packages/core/src/workers/WorkerPool.ts +167 -0
- package/packages/core/src/workers/agent.worker.ts +68 -0
- package/packages/core/src/workers/createWorker.ts +144 -0
- package/packages/core/src/workers/index.ts +5 -0
- package/packages/core/src/workers/workers.test.ts +48 -0
- package/test/setup-db.ts +2 -2
- package/tsconfig.json +2 -1
- package/.github/CODEOWNERS +0 -9
- package/.github/workflows/publish.yml +0 -89
- package/.github/workflows/version-bump.yml +0 -102
- package/bun.lock +0 -543
- package/bunfig.toml +0 -7
- package/packages/core/src/agent/providers.ts +0 -1
- 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
|
+
});
|