swarm-mail 0.1.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.
Files changed (36) hide show
  1. package/README.md +201 -0
  2. package/package.json +28 -0
  3. package/src/adapter.ts +306 -0
  4. package/src/index.ts +57 -0
  5. package/src/pglite.ts +189 -0
  6. package/src/streams/agent-mail.test.ts +777 -0
  7. package/src/streams/agent-mail.ts +535 -0
  8. package/src/streams/debug.test.ts +500 -0
  9. package/src/streams/debug.ts +727 -0
  10. package/src/streams/effect/ask.integration.test.ts +314 -0
  11. package/src/streams/effect/ask.ts +202 -0
  12. package/src/streams/effect/cursor.integration.test.ts +418 -0
  13. package/src/streams/effect/cursor.ts +288 -0
  14. package/src/streams/effect/deferred.test.ts +357 -0
  15. package/src/streams/effect/deferred.ts +445 -0
  16. package/src/streams/effect/index.ts +17 -0
  17. package/src/streams/effect/layers.ts +73 -0
  18. package/src/streams/effect/lock.test.ts +385 -0
  19. package/src/streams/effect/lock.ts +399 -0
  20. package/src/streams/effect/mailbox.test.ts +260 -0
  21. package/src/streams/effect/mailbox.ts +318 -0
  22. package/src/streams/events.test.ts +924 -0
  23. package/src/streams/events.ts +329 -0
  24. package/src/streams/index.test.ts +229 -0
  25. package/src/streams/index.ts +578 -0
  26. package/src/streams/migrations.test.ts +359 -0
  27. package/src/streams/migrations.ts +362 -0
  28. package/src/streams/projections.test.ts +611 -0
  29. package/src/streams/projections.ts +564 -0
  30. package/src/streams/store.integration.test.ts +658 -0
  31. package/src/streams/store.ts +1129 -0
  32. package/src/streams/swarm-mail.ts +552 -0
  33. package/src/types/adapter.ts +392 -0
  34. package/src/types/database.ts +127 -0
  35. package/src/types/index.ts +26 -0
  36. package/tsconfig.json +22 -0
@@ -0,0 +1,318 @@
1
+ /**
2
+ * DurableMailbox - Actor-style messaging with envelope pattern
3
+ *
4
+ * Combines DurableCursor (positioned consumption) with Envelope pattern for
5
+ * request/response messaging between agents.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const program = Effect.gen(function* () {
10
+ * const mailbox = yield* DurableMailbox;
11
+ * const myMailbox = yield* mailbox.create({ agent: "worker-1" });
12
+ *
13
+ * // Send message with optional reply channel
14
+ * yield* myMailbox.send("worker-2", {
15
+ * payload: { task: "process-data" },
16
+ * replyTo: "deferred:xyz"
17
+ * });
18
+ *
19
+ * // Receive messages
20
+ * for await (const envelope of myMailbox.receive()) {
21
+ * yield* handleMessage(envelope.payload);
22
+ * if (envelope.replyTo) {
23
+ * yield* DurableDeferred.resolve(envelope.replyTo, result);
24
+ * }
25
+ * yield* envelope.commit();
26
+ * }
27
+ * });
28
+ * ```
29
+ */
30
+ import { Context, Effect, Layer } from "effect";
31
+ import { DurableCursor, type Cursor } from "./cursor";
32
+ import { appendEvent } from "../store";
33
+ import type { MessageSentEvent } from "../events";
34
+
35
+ // ============================================================================
36
+ // Types
37
+ // ============================================================================
38
+
39
+ /**
40
+ * Envelope wrapping a message with metadata
41
+ */
42
+ export interface Envelope<T = unknown> {
43
+ /** Message payload */
44
+ readonly payload: T;
45
+ /** Optional URL of DurableDeferred for response */
46
+ readonly replyTo?: string;
47
+ /** Agent who sent the message */
48
+ readonly sender: string;
49
+ /** Original message ID */
50
+ readonly messageId: number;
51
+ /** Thread ID for conversation tracking */
52
+ readonly threadId?: string;
53
+ /** Commit this message position */
54
+ readonly commit: () => Effect.Effect<void>;
55
+ }
56
+
57
+ /**
58
+ * Configuration for creating a mailbox
59
+ */
60
+ export interface MailboxConfig {
61
+ /** Agent name (mailbox owner) */
62
+ readonly agent: string;
63
+ /** Project key for scoping messages */
64
+ readonly projectKey: string;
65
+ /** Optional project path for database location */
66
+ readonly projectPath?: string;
67
+ /** Batch size for reading messages (default: 100) */
68
+ readonly batchSize?: number;
69
+ }
70
+
71
+ /**
72
+ * Mailbox instance for an agent
73
+ */
74
+ export interface Mailbox {
75
+ /** Agent name */
76
+ readonly agent: string;
77
+ /** Send a message to another agent */
78
+ readonly send: <T>(
79
+ to: string | string[],
80
+ envelope: {
81
+ payload: T;
82
+ replyTo?: string;
83
+ threadId?: string;
84
+ importance?: "low" | "normal" | "high" | "urgent";
85
+ },
86
+ ) => Effect.Effect<void>;
87
+ /** Receive messages as async iterable */
88
+ readonly receive: <T = unknown>() => AsyncIterable<Envelope<T>>;
89
+ /** Peek at next message without consuming */
90
+ readonly peek: <T = unknown>() => Effect.Effect<Envelope<T> | null>;
91
+ }
92
+
93
+ /**
94
+ * DurableMailbox service interface
95
+ */
96
+ export interface DurableMailboxService {
97
+ /** Create a new mailbox instance */
98
+ readonly create: (
99
+ config: MailboxConfig,
100
+ ) => Effect.Effect<Mailbox, never, DurableCursor>;
101
+ }
102
+
103
+ // ============================================================================
104
+ // Service Definition
105
+ // ============================================================================
106
+
107
+ /**
108
+ * DurableMailbox Context.Tag
109
+ */
110
+ export class DurableMailbox extends Context.Tag("DurableMailbox")<
111
+ DurableMailbox,
112
+ DurableMailboxService
113
+ >() {}
114
+
115
+ // ============================================================================
116
+ // Implementation
117
+ // ============================================================================
118
+
119
+ /**
120
+ * Extract envelope from MessageSentEvent
121
+ */
122
+ function eventToEnvelope<T>(
123
+ event: MessageSentEvent & { id: number; sequence: number },
124
+ agentName: string,
125
+ commitFn: () => Effect.Effect<void>,
126
+ ): Envelope<T> | null {
127
+ // Filter: only messages addressed to this agent
128
+ if (!event.to_agents.includes(agentName)) {
129
+ return null;
130
+ }
131
+
132
+ // Parse body as envelope (assume JSON-encoded)
133
+ let payload: T;
134
+ let replyTo: string | undefined;
135
+ let sender = event.from_agent;
136
+
137
+ try {
138
+ // Body can be either:
139
+ // 1. Plain JSON payload (legacy)
140
+ // 2. Envelope JSON with { payload, replyTo?, sender? }
141
+ const parsed = JSON.parse(event.body);
142
+ if (parsed.payload !== undefined) {
143
+ // It's an envelope
144
+ payload = parsed.payload as T;
145
+ replyTo = parsed.replyTo;
146
+ sender = parsed.sender || event.from_agent;
147
+ } else {
148
+ // It's a plain payload
149
+ payload = parsed as T;
150
+ }
151
+ } catch {
152
+ // Body is not JSON, treat as string payload
153
+ payload = event.body as unknown as T;
154
+ }
155
+
156
+ return {
157
+ payload,
158
+ replyTo,
159
+ sender,
160
+ messageId: event.message_id || event.id,
161
+ threadId: event.thread_id,
162
+ commit: commitFn,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Create send function
168
+ */
169
+ function createSendFn(config: MailboxConfig): <T>(
170
+ to: string | string[],
171
+ envelope: {
172
+ payload: T;
173
+ replyTo?: string;
174
+ threadId?: string;
175
+ importance?: "low" | "normal" | "high" | "urgent";
176
+ },
177
+ ) => Effect.Effect<void> {
178
+ return <T>(
179
+ to: string | string[],
180
+ envelope: {
181
+ payload: T;
182
+ replyTo?: string;
183
+ threadId?: string;
184
+ importance?: "low" | "normal" | "high" | "urgent";
185
+ },
186
+ ): Effect.Effect<void> => {
187
+ return Effect.gen(function* () {
188
+ const toAgents = Array.isArray(to) ? to : [to];
189
+
190
+ // Create envelope body
191
+ const envelopeBody = {
192
+ payload: envelope.payload,
193
+ replyTo: envelope.replyTo,
194
+ sender: config.agent,
195
+ };
196
+
197
+ // Create MessageSentEvent
198
+ const event: Omit<
199
+ MessageSentEvent,
200
+ "id" | "sequence" | "timestamp" | "type"
201
+ > = {
202
+ project_key: config.projectKey,
203
+ from_agent: config.agent,
204
+ to_agents: toAgents,
205
+ subject: envelope.threadId || `msg-${Date.now()}`,
206
+ body: JSON.stringify(envelopeBody),
207
+ thread_id: envelope.threadId,
208
+ importance: envelope.importance || "normal",
209
+ ack_required: false,
210
+ };
211
+
212
+ // Append to event store
213
+ yield* Effect.promise(() =>
214
+ appendEvent(
215
+ {
216
+ type: "message_sent",
217
+ timestamp: Date.now(),
218
+ ...event,
219
+ },
220
+ config.projectPath,
221
+ ),
222
+ );
223
+ });
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Create receive function
229
+ */
230
+ function createReceiveFn(
231
+ cursor: Cursor,
232
+ agentName: string,
233
+ ): <T = unknown>() => AsyncIterable<Envelope<T>> {
234
+ return <T = unknown>(): AsyncIterable<Envelope<T>> => {
235
+ const messageStream = cursor.consume<
236
+ MessageSentEvent & { id: number; sequence: number }
237
+ >();
238
+
239
+ return {
240
+ async *[Symbol.asyncIterator]() {
241
+ for await (const msg of messageStream) {
242
+ const envelope = eventToEnvelope<T>(msg.value, agentName, msg.commit);
243
+ if (envelope) {
244
+ yield envelope;
245
+ } else {
246
+ // Not for this agent, skip and commit
247
+ await Effect.runPromise(msg.commit());
248
+ }
249
+ }
250
+ },
251
+ };
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Create peek function
257
+ */
258
+ function createPeekFn(
259
+ cursor: Cursor,
260
+ agentName: string,
261
+ ): <T = unknown>() => Effect.Effect<Envelope<T> | null> {
262
+ return <T = unknown>(): Effect.Effect<Envelope<T> | null> => {
263
+ return Effect.promise(async () => {
264
+ const messageStream = cursor.consume<
265
+ MessageSentEvent & { id: number; sequence: number }
266
+ >();
267
+
268
+ for await (const msg of messageStream) {
269
+ const envelope = eventToEnvelope<T>(msg.value, agentName, msg.commit);
270
+ if (envelope) {
271
+ return envelope;
272
+ }
273
+ // Not for this agent, skip and commit
274
+ await Effect.runPromise(msg.commit());
275
+ }
276
+
277
+ return null;
278
+ });
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Create mailbox implementation
284
+ */
285
+ function createMailboxImpl(
286
+ config: MailboxConfig,
287
+ ): Effect.Effect<Mailbox, never, DurableCursor> {
288
+ return Effect.gen(function* () {
289
+ const cursorService = yield* DurableCursor;
290
+
291
+ // Create cursor for this agent's messages
292
+ const cursor = yield* cursorService.create({
293
+ stream: `projects/${config.projectKey}/events`,
294
+ checkpoint: `agents/${config.agent}/mailbox`,
295
+ projectPath: config.projectPath,
296
+ batchSize: config.batchSize,
297
+ types: ["message_sent"], // Only read message_sent events
298
+ });
299
+
300
+ return {
301
+ agent: config.agent,
302
+ send: createSendFn(config),
303
+ receive: createReceiveFn(cursor, config.agent),
304
+ peek: createPeekFn(cursor, config.agent),
305
+ };
306
+ });
307
+ }
308
+
309
+ // ============================================================================
310
+ // Layer
311
+ // ============================================================================
312
+
313
+ /**
314
+ * Live implementation of DurableMailbox service
315
+ */
316
+ export const DurableMailboxLive = Layer.succeed(DurableMailbox, {
317
+ create: createMailboxImpl,
318
+ });