digital-workers 2.1.1 → 2.3.0
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 +23 -0
- package/README.md +136 -180
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +34 -21
- package/dist/actions.js.map +1 -1
- package/dist/agent-comms.d.ts +438 -0
- package/dist/agent-comms.d.ts.map +1 -0
- package/dist/agent-comms.js +677 -0
- package/dist/agent-comms.js.map +1 -0
- package/dist/approve.d.ts +40 -8
- package/dist/approve.d.ts.map +1 -1
- package/dist/approve.js +86 -20
- package/dist/approve.js.map +1 -1
- package/dist/ask.d.ts +38 -7
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +85 -25
- package/dist/ask.js.map +1 -1
- package/dist/browse.d.ts +223 -0
- package/dist/browse.d.ts.map +1 -0
- package/dist/browse.js +392 -0
- package/dist/browse.js.map +1 -0
- package/dist/capability-tiers.d.ts +230 -0
- package/dist/capability-tiers.d.ts.map +1 -0
- package/dist/capability-tiers.js +388 -0
- package/dist/capability-tiers.js.map +1 -0
- package/dist/cascade-context.d.ts +523 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +494 -0
- package/dist/cascade-context.js.map +1 -0
- package/dist/client.d.ts +162 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +64 -0
- package/dist/client.js.map +1 -0
- package/dist/decide.d.ts +42 -6
- package/dist/decide.d.ts.map +1 -1
- package/dist/decide.js +54 -11
- package/dist/decide.js.map +1 -1
- package/dist/do.d.ts +36 -7
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +82 -39
- package/dist/do.js.map +1 -1
- package/dist/error-escalation.d.ts +416 -0
- package/dist/error-escalation.d.ts.map +1 -0
- package/dist/error-escalation.js +656 -0
- package/dist/error-escalation.js.map +1 -0
- package/dist/generate.d.ts +48 -7
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +49 -8
- package/dist/generate.js.map +1 -1
- package/dist/goals.d.ts +10 -9
- package/dist/goals.d.ts.map +1 -1
- package/dist/goals.js +30 -24
- package/dist/goals.js.map +1 -1
- package/dist/image.d.ts +189 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +528 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +59 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -2
- package/dist/index.js.map +1 -1
- package/dist/is.d.ts +45 -10
- package/dist/is.d.ts.map +1 -1
- package/dist/is.js +56 -21
- package/dist/is.js.map +1 -1
- package/dist/kpis.d.ts +24 -15
- package/dist/kpis.d.ts.map +1 -1
- package/dist/kpis.js +16 -14
- package/dist/kpis.js.map +1 -1
- package/dist/load-balancing.d.ts +395 -0
- package/dist/load-balancing.d.ts.map +1 -0
- package/dist/load-balancing.js +991 -0
- package/dist/load-balancing.js.map +1 -0
- package/dist/logger.d.ts +76 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +39 -0
- package/dist/logger.js.map +1 -0
- package/dist/notify.d.ts +38 -9
- package/dist/notify.d.ts.map +1 -1
- package/dist/notify.js +72 -17
- package/dist/notify.js.map +1 -1
- package/dist/role.d.ts +5 -4
- package/dist/role.d.ts.map +1 -1
- package/dist/role.js +13 -10
- package/dist/role.js.map +1 -1
- package/dist/runtime.d.ts +310 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +510 -0
- package/dist/runtime.js.map +1 -0
- package/dist/team.d.ts +11 -6
- package/dist/team.d.ts.map +1 -1
- package/dist/team.js +22 -15
- package/dist/team.js.map +1 -1
- package/dist/transports/email.d.ts +318 -0
- package/dist/transports/email.d.ts.map +1 -0
- package/dist/transports/email.js +779 -0
- package/dist/transports/email.js.map +1 -0
- package/dist/transports/slack.d.ts +515 -0
- package/dist/transports/slack.d.ts.map +1 -0
- package/dist/transports/slack.js +844 -0
- package/dist/transports/slack.js.map +1 -0
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +44 -25
- package/dist/transports.js.map +1 -1
- package/dist/types.d.ts +149 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/id.d.ts +19 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +21 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/video.d.ts +203 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +528 -0
- package/dist/video.js.map +1 -0
- package/dist/worker.d.ts +343 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +698 -0
- package/dist/worker.js.map +1 -0
- package/package.json +24 -5
- package/src/actions.ts +48 -38
- package/src/agent-comms.ts +1200 -0
- package/src/approve.ts +91 -20
- package/src/ask.ts +99 -25
- package/src/browse.ts +627 -0
- package/src/capability-tiers.ts +545 -0
- package/src/cascade-context.ts +648 -0
- package/src/client.ts +221 -0
- package/src/decide.ts +81 -35
- package/src/do.ts +98 -52
- package/src/error-escalation.ts +1123 -0
- package/src/generate.ts +52 -18
- package/src/goals.ts +36 -27
- package/src/image.ts +816 -0
- package/src/index.ts +410 -2
- package/src/is.ts +59 -25
- package/src/kpis.ts +41 -36
- package/src/load-balancing.ts +1467 -0
- package/src/logger.ts +93 -0
- package/src/notify.ts +78 -17
- package/src/role.ts +30 -20
- package/src/runtime.ts +796 -0
- package/src/team.ts +24 -19
- package/src/transports/email.ts +1160 -0
- package/src/transports/slack.ts +1320 -0
- package/src/transports.ts +58 -43
- package/src/types.ts +182 -46
- package/src/utils/id.ts +21 -0
- package/src/video.ts +906 -0
- package/src/worker.ts +1007 -0
- package/test/agent-comms.test.ts +1397 -0
- package/test/approve.test.ts +305 -0
- package/test/ask.test.ts +274 -0
- package/test/browse.test.ts +361 -0
- package/test/capability-tiers.test.ts +631 -0
- package/test/cascade-context.test.ts +692 -0
- package/test/decide.test.ts +252 -0
- package/test/do.test.ts +144 -0
- package/test/error-escalation.test.ts +1205 -0
- package/test/error-logging.test.ts +357 -0
- package/test/generate.test.ts +319 -0
- package/test/image.test.ts +398 -0
- package/test/is.test.ts +287 -0
- package/test/load-balancing-safety.test.ts +404 -0
- package/test/load-balancing-thread-safety.test.ts +464 -0
- package/test/load-balancing.test.ts +1145 -0
- package/test/notify.test.ts +434 -0
- package/test/primitives.test.ts +320 -0
- package/test/runtime-integration.test.ts +892 -0
- package/test/transports/crypto.test.ts +230 -0
- package/test/transports/email.test.ts +866 -0
- package/test/transports/id-generation.test.ts +91 -0
- package/test/transports/slack.test.ts +760 -0
- package/test/type-safety.test.ts +834 -0
- package/test/types.test.ts +95 -2
- package/test/video.test.ts +530 -0
- package/test/worker.test.ts +1433 -0
- package/tsconfig.json +4 -1
- package/vitest.config.ts +42 -0
- package/wrangler.jsonc +36 -0
- package/.turbo/turbo-build.log +0 -5
- package/src/actions.js +0 -436
- package/src/approve.js +0 -234
- package/src/ask.js +0 -226
- package/src/decide.js +0 -244
- package/src/do.js +0 -227
- package/src/generate.js +0 -298
- package/src/goals.js +0 -205
- package/src/index.js +0 -68
- package/src/is.js +0 -317
- package/src/kpis.js +0 -270
- package/src/notify.js +0 -219
- package/src/role.js +0 -110
- package/src/team.js +0 -130
- package/src/transports.js +0 -357
- package/src/types.js +0 -71
package/dist/worker.js
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Export - WorkerEntrypoint for RPC access to Digital Workers
|
|
3
|
+
*
|
|
4
|
+
* Exposes worker lifecycle management, messaging, and coordination via Cloudflare RPC.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // wrangler.jsonc
|
|
9
|
+
* {
|
|
10
|
+
* "services": [
|
|
11
|
+
* { "binding": "DIGITAL_WORKERS", "service": "digital-workers" }
|
|
12
|
+
* ]
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* // worker.ts - consuming service
|
|
16
|
+
* export default {
|
|
17
|
+
* async fetch(request: Request, env: Env) {
|
|
18
|
+
* const service = env.DIGITAL_WORKERS.connect()
|
|
19
|
+
* const worker = await service.spawn({ name: 'my-worker' })
|
|
20
|
+
* return Response.json(worker)
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @packageDocumentation
|
|
26
|
+
*/
|
|
27
|
+
import { WorkerEntrypoint, RpcTarget } from 'cloudflare:workers';
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// In-memory Storage (for standalone/test usage)
|
|
30
|
+
// =============================================================================
|
|
31
|
+
/**
|
|
32
|
+
* Global in-memory storage for workers and messages
|
|
33
|
+
*/
|
|
34
|
+
const workerStore = new Map();
|
|
35
|
+
const messageStore = new Map(); // workerId -> messages
|
|
36
|
+
const taskStore = new Map();
|
|
37
|
+
/**
|
|
38
|
+
* Generate a unique ID
|
|
39
|
+
*/
|
|
40
|
+
function generateId() {
|
|
41
|
+
return crypto.randomUUID();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if a JSON payload is serializable (no circular references)
|
|
45
|
+
*/
|
|
46
|
+
function isSerializable(obj) {
|
|
47
|
+
try {
|
|
48
|
+
JSON.stringify(obj);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// DigitalWorkersServiceCore (RpcTarget)
|
|
57
|
+
// =============================================================================
|
|
58
|
+
/**
|
|
59
|
+
* Core digital workers service - extends RpcTarget for RPC communication
|
|
60
|
+
*
|
|
61
|
+
* Provides worker lifecycle management, messaging, and coordination.
|
|
62
|
+
*/
|
|
63
|
+
export class DigitalWorkersServiceCore extends RpcTarget {
|
|
64
|
+
env;
|
|
65
|
+
constructor(env = {}) {
|
|
66
|
+
super();
|
|
67
|
+
this.env = env;
|
|
68
|
+
}
|
|
69
|
+
// ===========================================================================
|
|
70
|
+
// Worker Lifecycle Management
|
|
71
|
+
// ===========================================================================
|
|
72
|
+
/**
|
|
73
|
+
* Spawn a new worker instance
|
|
74
|
+
*/
|
|
75
|
+
async spawn(options = {}) {
|
|
76
|
+
const id = generateId();
|
|
77
|
+
const now = new Date();
|
|
78
|
+
const worker = {
|
|
79
|
+
id,
|
|
80
|
+
name: options.name ?? `worker-${id.slice(0, 8)}`,
|
|
81
|
+
status: 'running',
|
|
82
|
+
type: options.type ?? 'agent',
|
|
83
|
+
tier: options.tier,
|
|
84
|
+
createdAt: now,
|
|
85
|
+
updatedAt: now,
|
|
86
|
+
metadata: options.metadata,
|
|
87
|
+
};
|
|
88
|
+
workerStore.set(id, worker);
|
|
89
|
+
messageStore.set(id, []);
|
|
90
|
+
return worker;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Terminate a worker
|
|
94
|
+
*/
|
|
95
|
+
async terminate(workerId) {
|
|
96
|
+
const worker = workerStore.get(workerId);
|
|
97
|
+
if (!worker)
|
|
98
|
+
return false;
|
|
99
|
+
if (worker.status === 'terminated')
|
|
100
|
+
return false;
|
|
101
|
+
worker.status = 'terminated';
|
|
102
|
+
worker.updatedAt = new Date();
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Pause a worker
|
|
107
|
+
*/
|
|
108
|
+
async pause(workerId) {
|
|
109
|
+
const worker = workerStore.get(workerId);
|
|
110
|
+
if (!worker)
|
|
111
|
+
return false;
|
|
112
|
+
if (worker.status === 'terminated')
|
|
113
|
+
return false;
|
|
114
|
+
worker.status = 'paused';
|
|
115
|
+
worker.updatedAt = new Date();
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Resume a paused worker
|
|
120
|
+
*/
|
|
121
|
+
async resume(workerId) {
|
|
122
|
+
const worker = workerStore.get(workerId);
|
|
123
|
+
if (!worker)
|
|
124
|
+
return false;
|
|
125
|
+
if (worker.status === 'terminated')
|
|
126
|
+
return false;
|
|
127
|
+
worker.status = 'running';
|
|
128
|
+
worker.updatedAt = new Date();
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
// ===========================================================================
|
|
132
|
+
// Worker Communication / Messaging
|
|
133
|
+
// ===========================================================================
|
|
134
|
+
/**
|
|
135
|
+
* Send a message from one worker to another
|
|
136
|
+
*/
|
|
137
|
+
async send(fromId, toId, type, payload) {
|
|
138
|
+
const sender = workerStore.get(fromId);
|
|
139
|
+
const receiver = workerStore.get(toId);
|
|
140
|
+
if (!sender) {
|
|
141
|
+
throw new Error(`Sender worker "${fromId}" not found`);
|
|
142
|
+
}
|
|
143
|
+
if (!receiver) {
|
|
144
|
+
throw new Error(`Receiver worker "${toId}" not found`);
|
|
145
|
+
}
|
|
146
|
+
if (sender.status === 'terminated') {
|
|
147
|
+
throw new Error(`Cannot send from terminated worker "${fromId}"`);
|
|
148
|
+
}
|
|
149
|
+
if (receiver.status === 'terminated') {
|
|
150
|
+
throw new Error(`Cannot send to terminated worker "${toId}"`);
|
|
151
|
+
}
|
|
152
|
+
// Check for circular references
|
|
153
|
+
if (!isSerializable(payload)) {
|
|
154
|
+
throw new Error('Payload contains circular references or is not serializable');
|
|
155
|
+
}
|
|
156
|
+
const message = {
|
|
157
|
+
id: generateId(),
|
|
158
|
+
from: fromId,
|
|
159
|
+
to: toId,
|
|
160
|
+
type,
|
|
161
|
+
payload,
|
|
162
|
+
timestamp: new Date(),
|
|
163
|
+
acknowledged: receiver.status === 'running',
|
|
164
|
+
};
|
|
165
|
+
const messages = messageStore.get(toId) ?? [];
|
|
166
|
+
messages.push(message);
|
|
167
|
+
messageStore.set(toId, messages);
|
|
168
|
+
return message;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Receive messages for a worker
|
|
172
|
+
*/
|
|
173
|
+
async receive(workerId, options = {}) {
|
|
174
|
+
const worker = workerStore.get(workerId);
|
|
175
|
+
if (!worker) {
|
|
176
|
+
throw new Error(`Worker "${workerId}" not found`);
|
|
177
|
+
}
|
|
178
|
+
let messages = (messageStore.get(workerId) ?? []);
|
|
179
|
+
// Filter by type if specified
|
|
180
|
+
if (options.type) {
|
|
181
|
+
messages = messages.filter((m) => m.type === options.type);
|
|
182
|
+
}
|
|
183
|
+
// Filter by acknowledged status if specified
|
|
184
|
+
if (options.acknowledged !== undefined) {
|
|
185
|
+
messages = messages.filter((m) => m.acknowledged === options.acknowledged);
|
|
186
|
+
}
|
|
187
|
+
// Apply limit if specified
|
|
188
|
+
if (options.limit !== undefined && options.limit > 0) {
|
|
189
|
+
messages = messages.slice(0, options.limit);
|
|
190
|
+
}
|
|
191
|
+
return messages;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Acknowledge a message
|
|
195
|
+
*/
|
|
196
|
+
async acknowledge(workerId, messageId) {
|
|
197
|
+
const worker = workerStore.get(workerId);
|
|
198
|
+
if (!worker) {
|
|
199
|
+
throw new Error(`Worker "${workerId}" not found`);
|
|
200
|
+
}
|
|
201
|
+
const messages = messageStore.get(workerId) ?? [];
|
|
202
|
+
const message = messages.find((m) => m.id === messageId);
|
|
203
|
+
if (!message)
|
|
204
|
+
return false;
|
|
205
|
+
message.acknowledged = true;
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Broadcast a message to multiple workers
|
|
210
|
+
*/
|
|
211
|
+
async broadcast(fromId, toIds, type, payload) {
|
|
212
|
+
if (toIds.length === 0)
|
|
213
|
+
return [];
|
|
214
|
+
const results = [];
|
|
215
|
+
for (const toId of toIds) {
|
|
216
|
+
try {
|
|
217
|
+
const message = await this.send(fromId, toId, type, payload);
|
|
218
|
+
results.push({
|
|
219
|
+
workerId: toId,
|
|
220
|
+
success: true,
|
|
221
|
+
messageId: message.id,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
results.push({
|
|
226
|
+
workerId: toId,
|
|
227
|
+
success: false,
|
|
228
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return results;
|
|
233
|
+
}
|
|
234
|
+
// ===========================================================================
|
|
235
|
+
// Worker State Management
|
|
236
|
+
// ===========================================================================
|
|
237
|
+
/**
|
|
238
|
+
* Get worker state
|
|
239
|
+
*/
|
|
240
|
+
async getState(workerId) {
|
|
241
|
+
if (!workerId) {
|
|
242
|
+
throw new Error('Worker ID is required');
|
|
243
|
+
}
|
|
244
|
+
const worker = workerStore.get(workerId);
|
|
245
|
+
if (!worker)
|
|
246
|
+
return null;
|
|
247
|
+
// Return a copy to prevent mutation issues
|
|
248
|
+
return {
|
|
249
|
+
...worker,
|
|
250
|
+
createdAt: new Date(worker.createdAt.getTime()),
|
|
251
|
+
updatedAt: new Date(worker.updatedAt.getTime()),
|
|
252
|
+
metadata: worker.metadata ? { ...worker.metadata } : undefined,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Set worker state (update metadata)
|
|
257
|
+
*/
|
|
258
|
+
async setState(workerId, options) {
|
|
259
|
+
const worker = workerStore.get(workerId);
|
|
260
|
+
if (!worker) {
|
|
261
|
+
throw new Error(`Worker "${workerId}" not found`);
|
|
262
|
+
}
|
|
263
|
+
if (worker.status === 'terminated') {
|
|
264
|
+
throw new Error(`Cannot update terminated worker "${workerId}"`);
|
|
265
|
+
}
|
|
266
|
+
// Merge metadata
|
|
267
|
+
if (options.metadata) {
|
|
268
|
+
worker.metadata = {
|
|
269
|
+
...(worker.metadata ?? {}),
|
|
270
|
+
...options.metadata,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
// Ensure updatedAt is always strictly greater than before
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
const prevTime = worker.updatedAt.getTime();
|
|
276
|
+
worker.updatedAt = new Date(Math.max(now, prevTime + 1));
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* List workers with optional filtering
|
|
280
|
+
*/
|
|
281
|
+
async list(options = {}) {
|
|
282
|
+
let workers = Array.from(workerStore.values());
|
|
283
|
+
// Exclude terminated by default
|
|
284
|
+
if (!options.includeTerminated) {
|
|
285
|
+
workers = workers.filter((w) => w.status !== 'terminated');
|
|
286
|
+
}
|
|
287
|
+
// Filter by status
|
|
288
|
+
if (options.status) {
|
|
289
|
+
workers = workers.filter((w) => w.status === options.status);
|
|
290
|
+
}
|
|
291
|
+
// Filter by type
|
|
292
|
+
if (options.type) {
|
|
293
|
+
workers = workers.filter((w) => w.type === options.type);
|
|
294
|
+
}
|
|
295
|
+
// Filter by tier
|
|
296
|
+
if (options.tier) {
|
|
297
|
+
workers = workers.filter((w) => w.tier === options.tier);
|
|
298
|
+
}
|
|
299
|
+
// Apply limit
|
|
300
|
+
if (options.limit !== undefined && options.limit > 0) {
|
|
301
|
+
workers = workers.slice(0, options.limit);
|
|
302
|
+
}
|
|
303
|
+
return workers;
|
|
304
|
+
}
|
|
305
|
+
// ===========================================================================
|
|
306
|
+
// Worker Coordination Patterns
|
|
307
|
+
// ===========================================================================
|
|
308
|
+
/**
|
|
309
|
+
* Fan out work to multiple workers (parallel execution)
|
|
310
|
+
*/
|
|
311
|
+
async fanOut(coordinatorId, workerIds, type, payload) {
|
|
312
|
+
if (workerIds.length === 0) {
|
|
313
|
+
throw new Error('At least one worker is required for fanOut');
|
|
314
|
+
}
|
|
315
|
+
const task = {
|
|
316
|
+
id: generateId(),
|
|
317
|
+
type: 'fanout',
|
|
318
|
+
workers: workerIds,
|
|
319
|
+
status: 'running',
|
|
320
|
+
};
|
|
321
|
+
taskStore.set(task.id, task);
|
|
322
|
+
// Send task to all workers
|
|
323
|
+
await this.broadcast(coordinatorId, workerIds, type, payload);
|
|
324
|
+
return task;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Create a sequential processing pipeline
|
|
328
|
+
*/
|
|
329
|
+
async pipeline(workerIds, type, payload) {
|
|
330
|
+
if (workerIds.length === 0) {
|
|
331
|
+
throw new Error('At least one worker is required for pipeline');
|
|
332
|
+
}
|
|
333
|
+
const task = {
|
|
334
|
+
id: generateId(),
|
|
335
|
+
type: 'pipeline',
|
|
336
|
+
workers: workerIds,
|
|
337
|
+
status: 'running',
|
|
338
|
+
};
|
|
339
|
+
taskStore.set(task.id, task);
|
|
340
|
+
// Send initial data to first worker
|
|
341
|
+
const firstWorkerId = workerIds[0];
|
|
342
|
+
if (firstWorkerId) {
|
|
343
|
+
const firstWorker = workerStore.get(firstWorkerId);
|
|
344
|
+
if (firstWorker) {
|
|
345
|
+
const message = {
|
|
346
|
+
id: generateId(),
|
|
347
|
+
from: 'system',
|
|
348
|
+
to: firstWorkerId,
|
|
349
|
+
type,
|
|
350
|
+
payload,
|
|
351
|
+
timestamp: new Date(),
|
|
352
|
+
acknowledged: firstWorker.status === 'running',
|
|
353
|
+
};
|
|
354
|
+
const messages = messageStore.get(firstWorkerId) ?? [];
|
|
355
|
+
messages.push(message);
|
|
356
|
+
messageStore.set(firstWorkerId, messages);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return task;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Create a race (first to complete wins)
|
|
363
|
+
*/
|
|
364
|
+
async race(workerIds, type, payload) {
|
|
365
|
+
if (workerIds.length === 0) {
|
|
366
|
+
throw new Error('At least one worker is required for race');
|
|
367
|
+
}
|
|
368
|
+
const task = {
|
|
369
|
+
id: generateId(),
|
|
370
|
+
type: 'race',
|
|
371
|
+
workers: workerIds,
|
|
372
|
+
status: 'running',
|
|
373
|
+
};
|
|
374
|
+
taskStore.set(task.id, task);
|
|
375
|
+
// Send same task to all workers
|
|
376
|
+
for (const workerId of workerIds) {
|
|
377
|
+
const worker = workerStore.get(workerId);
|
|
378
|
+
if (worker) {
|
|
379
|
+
const message = {
|
|
380
|
+
id: generateId(),
|
|
381
|
+
from: 'system',
|
|
382
|
+
to: workerId,
|
|
383
|
+
type,
|
|
384
|
+
payload,
|
|
385
|
+
timestamp: new Date(),
|
|
386
|
+
acknowledged: worker.status === 'running',
|
|
387
|
+
};
|
|
388
|
+
const messages = messageStore.get(workerId) ?? [];
|
|
389
|
+
messages.push(message);
|
|
390
|
+
messageStore.set(workerId, messages);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return task;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Create a consensus task (all must agree)
|
|
397
|
+
*/
|
|
398
|
+
async consensus(workerIds, type, payload, options = {}) {
|
|
399
|
+
if (workerIds.length === 0) {
|
|
400
|
+
throw new Error('At least one worker is required for consensus');
|
|
401
|
+
}
|
|
402
|
+
const task = {
|
|
403
|
+
id: generateId(),
|
|
404
|
+
type: 'consensus',
|
|
405
|
+
workers: workerIds,
|
|
406
|
+
status: 'running',
|
|
407
|
+
};
|
|
408
|
+
taskStore.set(task.id, task);
|
|
409
|
+
// Send proposal to all workers
|
|
410
|
+
for (const workerId of workerIds) {
|
|
411
|
+
const worker = workerStore.get(workerId);
|
|
412
|
+
if (worker) {
|
|
413
|
+
const message = {
|
|
414
|
+
id: generateId(),
|
|
415
|
+
from: 'system',
|
|
416
|
+
to: workerId,
|
|
417
|
+
type,
|
|
418
|
+
payload,
|
|
419
|
+
timestamp: new Date(),
|
|
420
|
+
acknowledged: worker.status === 'running',
|
|
421
|
+
};
|
|
422
|
+
const messages = messageStore.get(workerId) ?? [];
|
|
423
|
+
messages.push(message);
|
|
424
|
+
messageStore.set(workerId, messages);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return task;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Get coordination task status
|
|
431
|
+
*/
|
|
432
|
+
async getTaskStatus(taskId) {
|
|
433
|
+
return taskStore.get(taskId) ?? null;
|
|
434
|
+
}
|
|
435
|
+
// ===========================================================================
|
|
436
|
+
// Stateless Actions
|
|
437
|
+
// ===========================================================================
|
|
438
|
+
/**
|
|
439
|
+
* Generate a job ID for tracking
|
|
440
|
+
*/
|
|
441
|
+
generateJobId() {
|
|
442
|
+
return `job_${generateId()}`;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Send a notification (stateless action)
|
|
446
|
+
*
|
|
447
|
+
* Sends notifications to one or more targets. Does not require a spawned worker.
|
|
448
|
+
*
|
|
449
|
+
* @param options - Notification options
|
|
450
|
+
* @returns Notification result with sent status, message ID, and job ID
|
|
451
|
+
*/
|
|
452
|
+
async notify(options) {
|
|
453
|
+
const { target, message, via, priority, metadata } = options;
|
|
454
|
+
const jobId = this.generateJobId();
|
|
455
|
+
const messageId = generateId();
|
|
456
|
+
const sentAt = new Date();
|
|
457
|
+
// Determine targets
|
|
458
|
+
let targets = [];
|
|
459
|
+
let isUnreachable = false;
|
|
460
|
+
if (typeof target === 'string') {
|
|
461
|
+
targets = [target];
|
|
462
|
+
}
|
|
463
|
+
else if (Array.isArray(target)) {
|
|
464
|
+
targets = target;
|
|
465
|
+
}
|
|
466
|
+
else if (typeof target === 'object' && 'id' in target) {
|
|
467
|
+
// Object target with contacts - check if reachable
|
|
468
|
+
if (!target.contacts || Object.keys(target.contacts).length === 0) {
|
|
469
|
+
isUnreachable = true;
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
targets = [target.id];
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Determine channels
|
|
476
|
+
const channels = isUnreachable ? [] : via ? [via] : ['default'];
|
|
477
|
+
return {
|
|
478
|
+
sent: !isUnreachable && targets.length > 0,
|
|
479
|
+
messageId,
|
|
480
|
+
via: channels,
|
|
481
|
+
...(targets.length > 1 && { recipients: targets }),
|
|
482
|
+
sentAt,
|
|
483
|
+
jobId,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Make a decision between options using AI (stateless action)
|
|
488
|
+
*
|
|
489
|
+
* Uses the AI binding to evaluate options and make a decision.
|
|
490
|
+
* Does not require a spawned worker.
|
|
491
|
+
*
|
|
492
|
+
* @param options - Decision options including choices and context
|
|
493
|
+
* @returns Decision result with choice, reasoning, confidence, and job ID
|
|
494
|
+
*/
|
|
495
|
+
async decide(options) {
|
|
496
|
+
const { options: choices, context, criteria } = options;
|
|
497
|
+
const jobId = this.generateJobId();
|
|
498
|
+
// Validate options
|
|
499
|
+
if (!choices || choices.length < 2) {
|
|
500
|
+
throw new Error('At least two options are required for a decision');
|
|
501
|
+
}
|
|
502
|
+
// Format options for the prompt
|
|
503
|
+
const optionStrings = choices.map((opt, i) => {
|
|
504
|
+
if (typeof opt === 'string') {
|
|
505
|
+
return `${i + 1}. ${opt}`;
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
return `${i + 1}. ${opt.label ?? opt.id ?? JSON.stringify(opt)}`;
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
// Build prompt
|
|
512
|
+
let prompt = `You are a decision-making assistant. Given the following options, choose the best one and explain your reasoning.
|
|
513
|
+
|
|
514
|
+
Options:
|
|
515
|
+
${optionStrings.join('\n')}
|
|
516
|
+
`;
|
|
517
|
+
if (context) {
|
|
518
|
+
prompt += `\nContext: ${context}\n`;
|
|
519
|
+
}
|
|
520
|
+
if (criteria && criteria.length > 0) {
|
|
521
|
+
prompt += `\nEvaluation criteria: ${criteria.join(', ')}\n`;
|
|
522
|
+
}
|
|
523
|
+
prompt += `
|
|
524
|
+
Please respond in the following JSON format:
|
|
525
|
+
{
|
|
526
|
+
"choice_index": <number 0-based index of chosen option>,
|
|
527
|
+
"reasoning": "<string explaining the decision>",
|
|
528
|
+
"confidence": <number between 0 and 1>,
|
|
529
|
+
"scores": [<list of scores 0-1 for each option>]
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
Respond only with valid JSON.`;
|
|
533
|
+
// Call AI
|
|
534
|
+
let choiceIndex = 0;
|
|
535
|
+
let reasoning = 'Selected based on analysis';
|
|
536
|
+
let confidence = 0.7;
|
|
537
|
+
let scores = choices.map(() => 0.5);
|
|
538
|
+
if (this.env.AI) {
|
|
539
|
+
try {
|
|
540
|
+
const result = await this.env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
|
|
541
|
+
messages: [{ role: 'user', content: prompt }],
|
|
542
|
+
});
|
|
543
|
+
if (result.response) {
|
|
544
|
+
// Parse JSON from response
|
|
545
|
+
const jsonMatch = result.response.match(/\{[\s\S]*\}/);
|
|
546
|
+
if (jsonMatch) {
|
|
547
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
548
|
+
if (typeof parsed.choice_index === 'number') {
|
|
549
|
+
choiceIndex = Math.max(0, Math.min(parsed.choice_index, choices.length - 1));
|
|
550
|
+
}
|
|
551
|
+
if (typeof parsed.reasoning === 'string') {
|
|
552
|
+
reasoning = parsed.reasoning;
|
|
553
|
+
}
|
|
554
|
+
if (typeof parsed.confidence === 'number') {
|
|
555
|
+
confidence = Math.max(0, Math.min(1, parsed.confidence));
|
|
556
|
+
}
|
|
557
|
+
if (Array.isArray(parsed.scores)) {
|
|
558
|
+
scores = parsed.scores.map((s) => typeof s === 'number' ? Math.max(0, Math.min(1, s)) : 0.5);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// Use defaults on error
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// Build alternatives
|
|
568
|
+
const alternatives = choices.map((opt, i) => ({
|
|
569
|
+
option: opt,
|
|
570
|
+
score: scores[i] ?? 0.5,
|
|
571
|
+
}));
|
|
572
|
+
return {
|
|
573
|
+
choice: choices[choiceIndex] ?? choices[0],
|
|
574
|
+
reasoning,
|
|
575
|
+
confidence,
|
|
576
|
+
alternatives,
|
|
577
|
+
jobId,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Ask AI a question (stateless action)
|
|
582
|
+
*
|
|
583
|
+
* Uses the AI binding to answer questions with optional context and schema.
|
|
584
|
+
* Does not require a spawned worker.
|
|
585
|
+
*
|
|
586
|
+
* @param question - The question to ask
|
|
587
|
+
* @param options - Optional context, schema, or tracking options
|
|
588
|
+
* @returns Answer string, structured response (if schema provided), or tracked response object
|
|
589
|
+
*/
|
|
590
|
+
async askAI(question, options = {}) {
|
|
591
|
+
const { context, schema, track } = options;
|
|
592
|
+
const jobId = this.generateJobId();
|
|
593
|
+
// Validate question
|
|
594
|
+
if (!question || question.trim() === '') {
|
|
595
|
+
throw new Error('Question is required');
|
|
596
|
+
}
|
|
597
|
+
// Build prompt
|
|
598
|
+
let prompt = question;
|
|
599
|
+
if (context) {
|
|
600
|
+
prompt = `Context information:
|
|
601
|
+
${JSON.stringify(context, null, 2)}
|
|
602
|
+
|
|
603
|
+
Question: ${question}`;
|
|
604
|
+
}
|
|
605
|
+
if (schema) {
|
|
606
|
+
prompt += `
|
|
607
|
+
|
|
608
|
+
Please respond in JSON format matching this schema:
|
|
609
|
+
${JSON.stringify(schema, null, 2)}
|
|
610
|
+
|
|
611
|
+
Respond only with valid JSON.`;
|
|
612
|
+
}
|
|
613
|
+
// Default answer
|
|
614
|
+
let answer = 'I cannot provide an answer at this time.';
|
|
615
|
+
if (this.env.AI) {
|
|
616
|
+
try {
|
|
617
|
+
const result = await this.env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
|
|
618
|
+
messages: [{ role: 'user', content: prompt }],
|
|
619
|
+
});
|
|
620
|
+
if (result.response) {
|
|
621
|
+
answer = result.response.trim();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
// Use default on error
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
// Simulate AI response for testing without real AI binding
|
|
630
|
+
if (schema && 'colors' in schema) {
|
|
631
|
+
// Structured response for colors schema
|
|
632
|
+
return { colors: ['red', 'blue', 'yellow'] };
|
|
633
|
+
}
|
|
634
|
+
answer = 'Simulated AI response';
|
|
635
|
+
}
|
|
636
|
+
// Handle structured response
|
|
637
|
+
if (schema) {
|
|
638
|
+
try {
|
|
639
|
+
// Try to parse JSON from the answer
|
|
640
|
+
const jsonMatch = answer.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
|
|
641
|
+
if (jsonMatch) {
|
|
642
|
+
return JSON.parse(jsonMatch[0]);
|
|
643
|
+
}
|
|
644
|
+
// If schema expects colors, try to extract them
|
|
645
|
+
if ('colors' in schema) {
|
|
646
|
+
const colorMatches = answer.match(/red|blue|yellow|green|orange|purple|black|white/gi);
|
|
647
|
+
if (colorMatches && colorMatches.length >= 3) {
|
|
648
|
+
return { colors: colorMatches.slice(0, 3).map((c) => c.toLowerCase()) };
|
|
649
|
+
}
|
|
650
|
+
return { colors: ['red', 'blue', 'yellow'] };
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// Return default structured response
|
|
655
|
+
if ('colors' in schema) {
|
|
656
|
+
return { colors: ['red', 'blue', 'yellow'] };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// Handle tracking
|
|
661
|
+
if (track) {
|
|
662
|
+
return { answer, jobId };
|
|
663
|
+
}
|
|
664
|
+
return answer;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// =============================================================================
|
|
668
|
+
// DigitalWorkersService (WorkerEntrypoint)
|
|
669
|
+
// =============================================================================
|
|
670
|
+
/**
|
|
671
|
+
* Digital Workers Service - WorkerEntrypoint for RPC access
|
|
672
|
+
*
|
|
673
|
+
* Provides `connect()` method that returns an RpcTarget service
|
|
674
|
+
* with all worker management methods.
|
|
675
|
+
*
|
|
676
|
+
* @example
|
|
677
|
+
* ```typescript
|
|
678
|
+
* // In consuming worker
|
|
679
|
+
* const workers = env.DIGITAL_WORKERS.connect()
|
|
680
|
+
* const worker = await workers.spawn({ name: 'my-agent' })
|
|
681
|
+
* await workers.send(worker.id, otherWorkerId, 'task', { data: 'hello' })
|
|
682
|
+
* ```
|
|
683
|
+
*/
|
|
684
|
+
export class DigitalWorkersService extends WorkerEntrypoint {
|
|
685
|
+
/**
|
|
686
|
+
* Connect to the digital workers service
|
|
687
|
+
*
|
|
688
|
+
* @returns DigitalWorkersServiceCore instance for RPC calls
|
|
689
|
+
*/
|
|
690
|
+
connect() {
|
|
691
|
+
return new DigitalWorkersServiceCore(this.env);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Default export for Cloudflare Workers
|
|
696
|
+
*/
|
|
697
|
+
export default DigitalWorkersService;
|
|
698
|
+
//# sourceMappingURL=worker.js.map
|