@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,125 @@
|
|
|
1
|
+
import { createAllTools } from "../tools/index.ts"
|
|
2
|
+
import type { Config } from "../config/loader.ts"
|
|
3
|
+
|
|
4
|
+
type WorkerRunMessage = {
|
|
5
|
+
type: "run"
|
|
6
|
+
jobId: string
|
|
7
|
+
toolName: string
|
|
8
|
+
args: unknown
|
|
9
|
+
toolConfig: Record<string, unknown>
|
|
10
|
+
hiveConfig: Config
|
|
11
|
+
mainThreadToolNames: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type WorkerRpcResponse = {
|
|
15
|
+
type: "rpc_result"
|
|
16
|
+
rpcId: string
|
|
17
|
+
ok: boolean
|
|
18
|
+
result?: unknown
|
|
19
|
+
error?: SerializedError
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type SerializedError = {
|
|
23
|
+
name: string
|
|
24
|
+
message: string
|
|
25
|
+
stack?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const pendingRpc = new Map<string, {
|
|
29
|
+
resolve: (value: unknown) => void
|
|
30
|
+
reject: (error: Error) => void
|
|
31
|
+
}>()
|
|
32
|
+
|
|
33
|
+
function serializeError(error: unknown): SerializedError {
|
|
34
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
|
35
|
+
return {
|
|
36
|
+
name: err.name,
|
|
37
|
+
message: err.message,
|
|
38
|
+
stack: err.stack,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseArgs(args: unknown): Record<string, unknown> {
|
|
43
|
+
if (typeof args === "string") {
|
|
44
|
+
return JSON.parse(args) as Record<string, unknown>
|
|
45
|
+
}
|
|
46
|
+
if (args && typeof args === "object") {
|
|
47
|
+
return args as Record<string, unknown>
|
|
48
|
+
}
|
|
49
|
+
return {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function requestMainThreadTool(
|
|
53
|
+
jobId: string,
|
|
54
|
+
toolName: string,
|
|
55
|
+
args: unknown,
|
|
56
|
+
toolConfig: Record<string, unknown>
|
|
57
|
+
): Promise<unknown> {
|
|
58
|
+
const rpcId = `${jobId}:${crypto.randomUUID()}`
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
pendingRpc.set(rpcId, { resolve, reject })
|
|
61
|
+
postMessage({
|
|
62
|
+
type: "rpc_call",
|
|
63
|
+
rpcId,
|
|
64
|
+
jobId,
|
|
65
|
+
toolName,
|
|
66
|
+
args,
|
|
67
|
+
toolConfig,
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runTool(message: WorkerRunMessage): Promise<void> {
|
|
73
|
+
const startedAt = performance.now()
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const parsedArgs = parseArgs(message.args)
|
|
77
|
+
const forceMainThread = message.mainThreadToolNames.includes(message.toolName)
|
|
78
|
+
const allTools = forceMainThread ? [] : createAllTools(message.hiveConfig)
|
|
79
|
+
const tool = allTools.find((candidate) => candidate.name === message.toolName)
|
|
80
|
+
|
|
81
|
+
const result = tool?.execute
|
|
82
|
+
? await tool.execute(parsedArgs, { configurable: message.toolConfig })
|
|
83
|
+
: await requestMainThreadTool(message.jobId, message.toolName, parsedArgs, message.toolConfig)
|
|
84
|
+
|
|
85
|
+
postMessage({
|
|
86
|
+
type: "result",
|
|
87
|
+
jobId: message.jobId,
|
|
88
|
+
ok: true,
|
|
89
|
+
result,
|
|
90
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
91
|
+
})
|
|
92
|
+
} catch (error) {
|
|
93
|
+
postMessage({
|
|
94
|
+
type: "result",
|
|
95
|
+
jobId: message.jobId,
|
|
96
|
+
ok: false,
|
|
97
|
+
error: serializeError(error),
|
|
98
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
onmessage = (event: MessageEvent<WorkerRunMessage | WorkerRpcResponse>) => {
|
|
104
|
+
const message = event.data
|
|
105
|
+
|
|
106
|
+
if (message.type === "rpc_result") {
|
|
107
|
+
const pending = pendingRpc.get(message.rpcId)
|
|
108
|
+
if (!pending) return
|
|
109
|
+
|
|
110
|
+
pendingRpc.delete(message.rpcId)
|
|
111
|
+
if (message.ok) {
|
|
112
|
+
pending.resolve(message.result)
|
|
113
|
+
} else {
|
|
114
|
+
const error = new Error(message.error?.message || "Tool RPC failed")
|
|
115
|
+
error.name = message.error?.name || "ToolRpcError"
|
|
116
|
+
error.stack = message.error?.stack
|
|
117
|
+
pending.reject(error)
|
|
118
|
+
}
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (message.type === "run") {
|
|
123
|
+
void runTool(message)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getDb } from "../storage/SQLiteStorage
|
|
2
|
-
import {
|
|
3
|
-
import { logger } from "../utils/logger
|
|
1
|
+
import { getDb } from "../storage/SQLiteStorage";
|
|
2
|
+
import { loadProviderApiKey } from "../storage/crypto";
|
|
3
|
+
import { logger } from "../utils/logger";
|
|
4
4
|
|
|
5
5
|
export interface VoiceConfig {
|
|
6
6
|
voiceEnabled: boolean;
|
|
@@ -126,21 +126,8 @@ class VoiceService {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
private async getProviderApiKey(providerId: string): Promise<string | null> {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
SELECT api_key_encrypted, api_key_iv FROM providers WHERE id = ?
|
|
132
|
-
`).get(providerId) as { api_key_encrypted: string; api_key_iv: string } | undefined;
|
|
133
|
-
|
|
134
|
-
if (!provider?.api_key_encrypted) {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
return await decryptApiKey(provider.api_key_encrypted, provider.api_key_iv);
|
|
140
|
-
} catch (error) {
|
|
141
|
-
log.error(`Failed to decrypt API key for provider ${providerId}: ${(error as Error).message}`);
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
129
|
+
const apiKey = await loadProviderApiKey(providerId);
|
|
130
|
+
return apiKey || null;
|
|
144
131
|
}
|
|
145
132
|
|
|
146
133
|
private async transcribeWithGroq(audio: AudioInput, modelId: string): Promise<string> {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkerPool — Manages a pool of Bun Workers for parallel task execution.
|
|
3
|
+
*
|
|
4
|
+
* Creates workers on demand, reuses idle workers, and handles task queuing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logger } from "../utils/logger.ts";
|
|
8
|
+
import { createWorker, type WorkerConfig, type WorkerInstance } from "./createWorker.ts";
|
|
9
|
+
|
|
10
|
+
const log = logger.child("worker-pool");
|
|
11
|
+
|
|
12
|
+
export interface WorkerPoolConfig {
|
|
13
|
+
maxWorkers?: number;
|
|
14
|
+
taskTimeoutMs?: number;
|
|
15
|
+
workerConfig?: WorkerConfig;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PoolTask {
|
|
19
|
+
id: string;
|
|
20
|
+
message: string;
|
|
21
|
+
agentId?: string;
|
|
22
|
+
threadId?: string;
|
|
23
|
+
channel?: string;
|
|
24
|
+
systemPrompt?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PoolTaskResult {
|
|
28
|
+
taskId: string;
|
|
29
|
+
result: string;
|
|
30
|
+
error?: string;
|
|
31
|
+
durationMs: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class WorkerPool {
|
|
35
|
+
private maxWorkers: number;
|
|
36
|
+
private taskTimeoutMs: number;
|
|
37
|
+
private workerConfig: WorkerConfig;
|
|
38
|
+
private workers: Map<string, WorkerInstance> = new Map();
|
|
39
|
+
private idleWorkers: string[] = [];
|
|
40
|
+
private taskQueue: Array<{ task: PoolTask; resolve: (result: PoolTaskResult) => void }> = [];
|
|
41
|
+
private busyWorkers: Set<string> = new Set();
|
|
42
|
+
|
|
43
|
+
constructor(config: WorkerPoolConfig = {}) {
|
|
44
|
+
this.maxWorkers = config.maxWorkers ?? 4;
|
|
45
|
+
this.taskTimeoutMs = config.taskTimeoutMs ?? 120000;
|
|
46
|
+
this.workerConfig = config.workerConfig ?? { name: "pool-worker" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Execute a single task in a worker.
|
|
51
|
+
*/
|
|
52
|
+
async execute(task: PoolTask): Promise<PoolTaskResult> {
|
|
53
|
+
const worker = await this.acquireWorker();
|
|
54
|
+
const startedAt = performance.now();
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = await worker.run(task.message, {
|
|
58
|
+
threadId: task.threadId,
|
|
59
|
+
channel: task.channel,
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
taskId: task.id,
|
|
63
|
+
result,
|
|
64
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
65
|
+
};
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return {
|
|
68
|
+
taskId: task.id,
|
|
69
|
+
result: "",
|
|
70
|
+
error: (err as Error).message,
|
|
71
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
72
|
+
};
|
|
73
|
+
} finally {
|
|
74
|
+
this.releaseWorker(worker.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Execute multiple tasks in parallel, up to maxWorkers at a time.
|
|
80
|
+
*/
|
|
81
|
+
async executeBatch(tasks: PoolTask[]): Promise<PoolTaskResult[]> {
|
|
82
|
+
return Promise.all(tasks.map((task) => this.execute(task)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Execute tasks with a limit on concurrency.
|
|
87
|
+
*/
|
|
88
|
+
async executeWithConcurrency(tasks: PoolTask[], concurrency: number): Promise<PoolTaskResult[]> {
|
|
89
|
+
const results: PoolTaskResult[] = [];
|
|
90
|
+
const executing = new Set<Promise<void>>();
|
|
91
|
+
|
|
92
|
+
for (const task of tasks) {
|
|
93
|
+
const promise = this.execute(task).then((result) => {
|
|
94
|
+
results.push(result);
|
|
95
|
+
});
|
|
96
|
+
executing.add(promise);
|
|
97
|
+
|
|
98
|
+
if (executing.size >= concurrency) {
|
|
99
|
+
await Promise.race(executing);
|
|
100
|
+
executing.delete(promise);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await Promise.all(executing);
|
|
105
|
+
return results;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get or create a worker.
|
|
110
|
+
*/
|
|
111
|
+
private acquireWorker(): Promise<WorkerInstance> {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
// Try to reuse an idle worker
|
|
114
|
+
if (this.idleWorkers.length > 0) {
|
|
115
|
+
const workerId = this.idleWorkers.pop()!;
|
|
116
|
+
this.busyWorkers.add(workerId);
|
|
117
|
+
resolve(this.workers.get(workerId)!);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Create a new worker if under limit
|
|
122
|
+
if (this.workers.size < this.maxWorkers) {
|
|
123
|
+
const worker = createWorker(this.workerConfig);
|
|
124
|
+
this.workers.set(worker.id, worker);
|
|
125
|
+
this.busyWorkers.add(worker.id);
|
|
126
|
+
resolve(worker);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Queue the request
|
|
131
|
+
const checkInterval = setInterval(() => {
|
|
132
|
+
if (this.idleWorkers.length > 0) {
|
|
133
|
+
clearInterval(checkInterval);
|
|
134
|
+
const workerId = this.idleWorkers.pop()!;
|
|
135
|
+
this.busyWorkers.add(workerId);
|
|
136
|
+
resolve(this.workers.get(workerId)!);
|
|
137
|
+
}
|
|
138
|
+
}, 50);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private releaseWorker(workerId: string): void {
|
|
143
|
+
this.busyWorkers.delete(workerId);
|
|
144
|
+
this.idleWorkers.push(workerId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Terminate all workers in the pool.
|
|
149
|
+
*/
|
|
150
|
+
shutdown(): void {
|
|
151
|
+
log.info(`[WorkerPool] Shutting down ${this.workers.size} workers`);
|
|
152
|
+
for (const worker of this.workers.values()) {
|
|
153
|
+
worker.terminate();
|
|
154
|
+
}
|
|
155
|
+
this.workers.clear();
|
|
156
|
+
this.idleWorkers = [];
|
|
157
|
+
this.busyWorkers.clear();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get stats(): { total: number; busy: number; idle: number } {
|
|
161
|
+
return {
|
|
162
|
+
total: this.workers.size,
|
|
163
|
+
busy: this.busyWorkers.size,
|
|
164
|
+
idle: this.idleWorkers.length,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Worker — Bun Worker that runs an agent task in isolation.
|
|
3
|
+
*
|
|
4
|
+
* Receives: { type: "AGENT_TASK", taskId, message, agentId, threadId, channel, systemPrompt }
|
|
5
|
+
* Sends: { type: "AGENT_RESULT", taskId, result } | { type: "AGENT_CHUNK", taskId, chunk }
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { runAgent } from "../agent/AgentRunner.ts";
|
|
9
|
+
import type { StreamChunk } from "../agent/AgentRunner.ts";
|
|
10
|
+
|
|
11
|
+
declare var self: {
|
|
12
|
+
onmessage: ((event: { data: WorkerMessage }) => void) | null;
|
|
13
|
+
postMessage(message: WorkerResponse): void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type WorkerMessage = {
|
|
17
|
+
type: "AGENT_TASK";
|
|
18
|
+
taskId: string;
|
|
19
|
+
message: string;
|
|
20
|
+
agentId: string;
|
|
21
|
+
threadId?: string;
|
|
22
|
+
channel?: string;
|
|
23
|
+
systemPrompt?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type WorkerResponse =
|
|
27
|
+
| { type: "AGENT_CHUNK"; taskId: string; chunk: StreamChunk }
|
|
28
|
+
| { type: "AGENT_RESULT"; taskId: string; result: string; error?: string };
|
|
29
|
+
|
|
30
|
+
self.onmessage = async (event) => {
|
|
31
|
+
const { type, taskId, message, agentId, threadId, channel, systemPrompt } = event.data;
|
|
32
|
+
|
|
33
|
+
if (type !== "AGENT_TASK") return;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const stream = runAgent({
|
|
37
|
+
agentId,
|
|
38
|
+
userMessage: message,
|
|
39
|
+
threadId: threadId ?? `worker-${taskId}`,
|
|
40
|
+
channel: channel ?? "worker",
|
|
41
|
+
isolated: true,
|
|
42
|
+
systemPromptOverride: systemPrompt,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let fullResponse = "";
|
|
46
|
+
|
|
47
|
+
for await (const chunk of stream) {
|
|
48
|
+
self.postMessage({ type: "AGENT_CHUNK", taskId, chunk });
|
|
49
|
+
|
|
50
|
+
if (chunk.agent?.messages) {
|
|
51
|
+
for (const msg of chunk.agent.messages) {
|
|
52
|
+
if (msg.content && typeof msg.content === "string") {
|
|
53
|
+
fullResponse = msg.content;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
self.postMessage({ type: "AGENT_RESULT", taskId, result: fullResponse });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
self.postMessage({
|
|
62
|
+
type: "AGENT_RESULT",
|
|
63
|
+
taskId,
|
|
64
|
+
result: "",
|
|
65
|
+
error: (err as Error).message,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createWorker — Factory for creating specialized Bun Workers.
|
|
3
|
+
*
|
|
4
|
+
* Creates a dedicated worker thread that runs an agent with a custom system prompt.
|
|
5
|
+
* Useful for parallel task execution and specialized agent roles.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { logger } from "../utils/logger.ts";
|
|
9
|
+
|
|
10
|
+
const log = logger.child("createWorker");
|
|
11
|
+
|
|
12
|
+
export interface WorkerConfig {
|
|
13
|
+
name: string;
|
|
14
|
+
agentId?: string;
|
|
15
|
+
systemPrompt?: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
provider?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WorkerInstance {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly id: string;
|
|
23
|
+
run(message: string, opts?: { threadId?: string; channel?: string }): Promise<string>;
|
|
24
|
+
runStream(message: string, opts?: { threadId?: string; channel?: string }): AsyncGenerator<WorkerChunk>;
|
|
25
|
+
terminate(): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WorkerChunk {
|
|
29
|
+
type: "chunk" | "result" | "error";
|
|
30
|
+
content?: string;
|
|
31
|
+
chunk?: any;
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const WORKER_EXT = import.meta.url.endsWith(".ts") ? ".worker.ts" : ".worker.js";
|
|
36
|
+
|
|
37
|
+
function resolveWorkerPath(): string {
|
|
38
|
+
return new URL(`./agent${WORKER_EXT}`, import.meta.url).pathname;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createWorker(config: WorkerConfig): WorkerInstance {
|
|
42
|
+
const workerId = `${config.name}-${crypto.randomUUID().slice(0, 8)}`;
|
|
43
|
+
const workerPath = resolveWorkerPath();
|
|
44
|
+
|
|
45
|
+
log.info(`[createWorker] Spawning worker ${workerId} from ${workerPath}`);
|
|
46
|
+
|
|
47
|
+
const worker = new Worker(workerPath, { smol: true });
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
name: config.name,
|
|
51
|
+
id: workerId,
|
|
52
|
+
|
|
53
|
+
async run(message, opts) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const taskId = crypto.randomUUID();
|
|
56
|
+
const timeout = setTimeout(() => {
|
|
57
|
+
reject(new Error(`Worker ${workerId} timed out`));
|
|
58
|
+
}, 120000);
|
|
59
|
+
|
|
60
|
+
const handler = (event: MessageEvent) => {
|
|
61
|
+
const data = event.data;
|
|
62
|
+
if (data.taskId !== taskId) return;
|
|
63
|
+
|
|
64
|
+
if (data.type === "AGENT_RESULT") {
|
|
65
|
+
clearTimeout(timeout);
|
|
66
|
+
worker.removeEventListener("message", handler);
|
|
67
|
+
if (data.error) {
|
|
68
|
+
reject(new Error(data.error));
|
|
69
|
+
} else {
|
|
70
|
+
resolve(data.result);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
worker.addEventListener("message", handler);
|
|
76
|
+
worker.postMessage({
|
|
77
|
+
type: "AGENT_TASK",
|
|
78
|
+
taskId,
|
|
79
|
+
message,
|
|
80
|
+
agentId: config.agentId ?? config.name,
|
|
81
|
+
threadId: opts?.threadId,
|
|
82
|
+
channel: opts?.channel ?? config.name,
|
|
83
|
+
systemPrompt: config.systemPrompt,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async *runStream(message, opts) {
|
|
89
|
+
const taskId = crypto.randomUUID();
|
|
90
|
+
|
|
91
|
+
worker.postMessage({
|
|
92
|
+
type: "AGENT_TASK",
|
|
93
|
+
taskId,
|
|
94
|
+
message,
|
|
95
|
+
agentId: config.agentId ?? config.name,
|
|
96
|
+
threadId: opts?.threadId,
|
|
97
|
+
channel: opts?.channel ?? config.name,
|
|
98
|
+
systemPrompt: config.systemPrompt,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
let resolved = false;
|
|
102
|
+
const pending: WorkerResponse[] = [];
|
|
103
|
+
|
|
104
|
+
const handler = (event: MessageEvent) => {
|
|
105
|
+
const data = event.data as WorkerResponse;
|
|
106
|
+
if (data.taskId !== taskId) return;
|
|
107
|
+
pending.push(data);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
worker.addEventListener("message", handler);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
while (!resolved) {
|
|
114
|
+
while (pending.length === 0) {
|
|
115
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
116
|
+
}
|
|
117
|
+
const data = pending.shift()!;
|
|
118
|
+
|
|
119
|
+
if (data.type === "AGENT_CHUNK") {
|
|
120
|
+
yield { type: "chunk" as const, chunk: data.chunk };
|
|
121
|
+
} else if (data.type === "AGENT_RESULT") {
|
|
122
|
+
resolved = true;
|
|
123
|
+
if (data.error) {
|
|
124
|
+
yield { type: "error" as const, error: data.error };
|
|
125
|
+
} else {
|
|
126
|
+
yield { type: "result" as const, content: data.result };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} finally {
|
|
131
|
+
worker.removeEventListener("message", handler);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
terminate() {
|
|
136
|
+
log.info(`[createWorker] Terminating worker ${workerId}`);
|
|
137
|
+
worker.terminate();
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
type WorkerResponse =
|
|
143
|
+
| { type: "AGENT_CHUNK"; taskId: string; chunk: any }
|
|
144
|
+
| { type: "AGENT_RESULT"; taskId: string; result: string; error?: string };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { createWorker, WorkerPool } from "./index.ts";
|
|
3
|
+
import { setupTestDb, teardownTestDb, insertTestAgent, insertTestProvider } from "../../../../test/setup-db.ts";
|
|
4
|
+
|
|
5
|
+
describe("createWorker", () => {
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
setupTestDb();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterAll(() => {
|
|
11
|
+
teardownTestDb();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("creates a worker instance with config", () => {
|
|
15
|
+
const worker = createWorker({
|
|
16
|
+
name: "test-worker",
|
|
17
|
+
systemPrompt: "You are a test worker.",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(worker.name).toBe("test-worker");
|
|
21
|
+
expect(worker.id).toContain("test-worker-");
|
|
22
|
+
worker.terminate();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("WorkerPool creates and manages workers", () => {
|
|
26
|
+
const pool = new WorkerPool({ maxWorkers: 2 });
|
|
27
|
+
expect(pool.stats.total).toBe(0);
|
|
28
|
+
expect(pool.stats.idle).toBe(0);
|
|
29
|
+
expect(pool.stats.busy).toBe(0);
|
|
30
|
+
pool.shutdown();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("WorkerPool executes tasks with limited concurrency", async () => {
|
|
34
|
+
const pool = new WorkerPool({
|
|
35
|
+
maxWorkers: 2,
|
|
36
|
+
workerConfig: { name: "pool-test" },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const tasks = Array.from({ length: 4 }, (_, i) => ({
|
|
40
|
+
id: `task-${i}`,
|
|
41
|
+
message: `Test message ${i}`,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// Since we don't have a real LLM in tests, we just verify the pool structure
|
|
45
|
+
expect(tasks).toHaveLength(4);
|
|
46
|
+
pool.shutdown();
|
|
47
|
+
});
|
|
48
|
+
});
|
package/test/setup-db.ts
CHANGED
|
@@ -195,7 +195,7 @@ export function insertTestPlaybookRule(overrides: Record<string, any> = {}): num
|
|
|
195
195
|
overrides.active ?? 1
|
|
196
196
|
);
|
|
197
197
|
|
|
198
|
-
return result.
|
|
198
|
+
return result.lastInsertRowid as number;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
export function insertTestConversation(threadId: string, role: string, content: string): number {
|
|
@@ -212,5 +212,5 @@ export function insertTestConversation(threadId: string, role: string, content:
|
|
|
212
212
|
Math.floor(content.length / 4)
|
|
213
213
|
);
|
|
214
214
|
|
|
215
|
-
return result.
|
|
215
|
+
return result.lastInsertRowid as number;
|
|
216
216
|
}
|
package/tsconfig.json
CHANGED