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.
- package/README.md +201 -0
- package/package.json +28 -0
- package/src/adapter.ts +306 -0
- package/src/index.ts +57 -0
- package/src/pglite.ts +189 -0
- package/src/streams/agent-mail.test.ts +777 -0
- package/src/streams/agent-mail.ts +535 -0
- package/src/streams/debug.test.ts +500 -0
- package/src/streams/debug.ts +727 -0
- package/src/streams/effect/ask.integration.test.ts +314 -0
- package/src/streams/effect/ask.ts +202 -0
- package/src/streams/effect/cursor.integration.test.ts +418 -0
- package/src/streams/effect/cursor.ts +288 -0
- package/src/streams/effect/deferred.test.ts +357 -0
- package/src/streams/effect/deferred.ts +445 -0
- package/src/streams/effect/index.ts +17 -0
- package/src/streams/effect/layers.ts +73 -0
- package/src/streams/effect/lock.test.ts +385 -0
- package/src/streams/effect/lock.ts +399 -0
- package/src/streams/effect/mailbox.test.ts +260 -0
- package/src/streams/effect/mailbox.ts +318 -0
- package/src/streams/events.test.ts +924 -0
- package/src/streams/events.ts +329 -0
- package/src/streams/index.test.ts +229 -0
- package/src/streams/index.ts +578 -0
- package/src/streams/migrations.test.ts +359 -0
- package/src/streams/migrations.ts +362 -0
- package/src/streams/projections.test.ts +611 -0
- package/src/streams/projections.ts +564 -0
- package/src/streams/store.integration.test.ts +658 -0
- package/src/streams/store.ts +1129 -0
- package/src/streams/swarm-mail.ts +552 -0
- package/src/types/adapter.ts +392 -0
- package/src/types/database.ts +127 -0
- package/src/types/index.ts +26 -0
- package/tsconfig.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# swarm-mail
|
|
2
|
+
|
|
3
|
+
Event sourcing primitives for multi-agent coordination. Local-first, no external servers.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
7
|
+
│ SWARM MAIL STACK │
|
|
8
|
+
├─────────────────────────────────────────────────────────────┤
|
|
9
|
+
│ TIER 3: COORDINATION │
|
|
10
|
+
│ └── ask<Req, Res>() - Request/Response (RPC-style) │
|
|
11
|
+
│ │
|
|
12
|
+
│ TIER 2: PATTERNS │
|
|
13
|
+
│ ├── DurableMailbox - Actor inbox with typed envelopes │
|
|
14
|
+
│ └── DurableLock - CAS-based mutual exclusion │
|
|
15
|
+
│ │
|
|
16
|
+
│ TIER 1: PRIMITIVES │
|
|
17
|
+
│ ├── DurableCursor - Checkpointed stream reader │
|
|
18
|
+
│ └── DurableDeferred - Distributed promise │
|
|
19
|
+
│ │
|
|
20
|
+
│ STORAGE │
|
|
21
|
+
│ └── PGLite (Embedded Postgres) + Migrations │
|
|
22
|
+
└─────────────────────────────────────────────────────────────┘
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bun add swarm-mail
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Event Store
|
|
34
|
+
|
|
35
|
+
Append-only event log with automatic projection updates:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { getSwarmMail } from "swarm-mail";
|
|
39
|
+
|
|
40
|
+
// Create swarm mail instance (automatically creates PGlite adapter)
|
|
41
|
+
const swarmMail = await getSwarmMail("/my/project");
|
|
42
|
+
|
|
43
|
+
// Append events
|
|
44
|
+
await swarmMail.appendEvent({
|
|
45
|
+
type: "agent_registered",
|
|
46
|
+
agent_name: "WorkerA",
|
|
47
|
+
task_description: "Implementing auth",
|
|
48
|
+
timestamp: Date.now(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Query projections
|
|
52
|
+
const agents = await swarmMail.getAgents();
|
|
53
|
+
const messages = await swarmMail.getInbox("WorkerA", { limit: 5 });
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Durable Primitives (Effect-TS)
|
|
57
|
+
|
|
58
|
+
Built on Effect-TS for type-safe, composable coordination:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { DurableMailbox, DurableLock, ask } from 'swarm-mail'
|
|
62
|
+
import { Effect } from 'effect'
|
|
63
|
+
|
|
64
|
+
// Actor mailbox
|
|
65
|
+
const mailbox = DurableMailbox.create<MyMessage>('worker-a')
|
|
66
|
+
await Effect.runPromise(
|
|
67
|
+
mailbox.send({ type: 'task', payload: 'do something' })
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// File locking
|
|
71
|
+
const lock = DurableLock.create('src/auth.ts')
|
|
72
|
+
await Effect.runPromise(
|
|
73
|
+
lock.acquire({ ttl: 60000 }).pipe(
|
|
74
|
+
Effect.flatMap(() => /* do work */),
|
|
75
|
+
Effect.ensuring(lock.release())
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// Request/response
|
|
80
|
+
const response = await Effect.runPromise(
|
|
81
|
+
ask<Request, Response>('other-agent', { type: 'get-types' })
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Database Adapter
|
|
86
|
+
|
|
87
|
+
Dependency injection for testing and flexibility:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { DatabaseAdapter, createSwarmMailAdapter } from 'swarm-mail'
|
|
91
|
+
|
|
92
|
+
// Implement your own adapter
|
|
93
|
+
const customAdapter: DatabaseAdapter = {
|
|
94
|
+
query: async (sql, params) => /* ... */,
|
|
95
|
+
exec: async (sql) => /* ... */,
|
|
96
|
+
transaction: async (fn) => /* ... */,
|
|
97
|
+
close: async () => /* ... */
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Use custom adapter
|
|
101
|
+
const swarmMail = createSwarmMailAdapter(customAdapter, '/my/project')
|
|
102
|
+
|
|
103
|
+
// Or use the convenience layer (built-in PGLite)
|
|
104
|
+
import { getSwarmMail, createInMemorySwarmMail } from 'swarm-mail'
|
|
105
|
+
const swarmMail = await getSwarmMail('/my/project') // persistent
|
|
106
|
+
const swarmMail = await createInMemorySwarmMail() // in-memory
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Event Types
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
type SwarmMailEvent =
|
|
113
|
+
| { type: "agent_registered"; agent_name: string; task_description?: string }
|
|
114
|
+
| {
|
|
115
|
+
type: "message_sent";
|
|
116
|
+
from: string;
|
|
117
|
+
to: string[];
|
|
118
|
+
subject: string;
|
|
119
|
+
body: string;
|
|
120
|
+
}
|
|
121
|
+
| { type: "message_read"; message_id: number; agent_name: string }
|
|
122
|
+
| {
|
|
123
|
+
type: "file_reserved";
|
|
124
|
+
agent_name: string;
|
|
125
|
+
paths: string[];
|
|
126
|
+
exclusive: boolean;
|
|
127
|
+
}
|
|
128
|
+
| { type: "file_released"; agent_name: string; paths: string[] }
|
|
129
|
+
| {
|
|
130
|
+
type: "swarm_checkpointed";
|
|
131
|
+
epic_id: string;
|
|
132
|
+
progress: number;
|
|
133
|
+
state: object;
|
|
134
|
+
}
|
|
135
|
+
| { type: "decomposition_generated"; epic_id: string; subtasks: object[] }
|
|
136
|
+
| {
|
|
137
|
+
type: "subtask_outcome";
|
|
138
|
+
bead_id: string;
|
|
139
|
+
success: boolean;
|
|
140
|
+
duration_ms: number;
|
|
141
|
+
};
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Projections
|
|
145
|
+
|
|
146
|
+
Materialized views derived from events:
|
|
147
|
+
|
|
148
|
+
| Projection | Description |
|
|
149
|
+
| ------------------- | ---------------------------------- |
|
|
150
|
+
| `agents` | Active agents per project |
|
|
151
|
+
| `messages` | Agent inbox/outbox with recipients |
|
|
152
|
+
| `file_reservations` | Current file locks with TTL |
|
|
153
|
+
| `swarm_contexts` | Checkpoint state for recovery |
|
|
154
|
+
| `eval_records` | Outcome data for learning |
|
|
155
|
+
|
|
156
|
+
## Architecture
|
|
157
|
+
|
|
158
|
+
- **Append-only log** - Events are immutable, projections are derived
|
|
159
|
+
- **Local-first** - PGLite embedded Postgres, no external servers
|
|
160
|
+
- **Effect-TS** - Type-safe, composable, testable
|
|
161
|
+
- **Exactly-once** - DurableCursor checkpoints position
|
|
162
|
+
|
|
163
|
+
## API Reference
|
|
164
|
+
|
|
165
|
+
### SwarmMailAdapter
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
interface SwarmMailAdapter {
|
|
169
|
+
// Events
|
|
170
|
+
appendEvent(event: SwarmMailEvent): Promise<{ id: number; sequence: number }>;
|
|
171
|
+
getEvents(options?: {
|
|
172
|
+
limit?: number;
|
|
173
|
+
after?: number;
|
|
174
|
+
}): Promise<StoredEvent[]>;
|
|
175
|
+
|
|
176
|
+
// Agents
|
|
177
|
+
getAgents(): Promise<Agent[]>;
|
|
178
|
+
getAgent(name: string): Promise<Agent | null>;
|
|
179
|
+
|
|
180
|
+
// Messages
|
|
181
|
+
getInbox(agent: string, options?: InboxOptions): Promise<Message[]>;
|
|
182
|
+
getMessage(id: number): Promise<Message | null>;
|
|
183
|
+
getThread(threadId: string): Promise<Message[]>;
|
|
184
|
+
|
|
185
|
+
// Reservations
|
|
186
|
+
getReservations(): Promise<Reservation[]>;
|
|
187
|
+
getReservationsForAgent(agent: string): Promise<Reservation[]>;
|
|
188
|
+
checkConflicts(paths: string[], excludeAgent?: string): Promise<Conflict[]>;
|
|
189
|
+
|
|
190
|
+
// Swarm Context
|
|
191
|
+
getSwarmContext(epicId: string): Promise<SwarmContext | null>;
|
|
192
|
+
|
|
193
|
+
// Debug
|
|
194
|
+
debugEvents(options?: DebugOptions): Promise<DebugEvent[]>;
|
|
195
|
+
debugAgent(name: string): Promise<AgentDebugInfo>;
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "swarm-mail",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node && tsc",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"effect": "^3.19.12",
|
|
20
|
+
"@electric-sql/pglite": "0.3.14",
|
|
21
|
+
"zod": "4.1.8"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"bun-types": "^1.3.4",
|
|
25
|
+
"typescript": "^5.7.2",
|
|
26
|
+
"vitest": "^2.1.8"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SwarmMail Adapter - Factory for creating SwarmMailAdapter instances
|
|
3
|
+
*
|
|
4
|
+
* This file implements the adapter pattern for swarm-mail, enabling
|
|
5
|
+
* dependency injection of the database instead of singleton access.
|
|
6
|
+
*
|
|
7
|
+
* ## Design Pattern
|
|
8
|
+
* - Accept DatabaseAdapter via factory parameter
|
|
9
|
+
* - Return SwarmMailAdapter interface
|
|
10
|
+
* - Delegate to internal implementation functions
|
|
11
|
+
* - No direct database access (all via adapter)
|
|
12
|
+
*
|
|
13
|
+
* ## Usage
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { createPGLiteAdapter } from '@opencode/swarm-mail/adapters/pglite';
|
|
16
|
+
* import { createSwarmMailAdapter } from '@opencode/swarm-mail';
|
|
17
|
+
*
|
|
18
|
+
* const dbAdapter = createPGLiteAdapter({ path: './streams.db' });
|
|
19
|
+
* const swarmMail = createSwarmMailAdapter(dbAdapter, '/path/to/project');
|
|
20
|
+
*
|
|
21
|
+
* // Use the adapter
|
|
22
|
+
* await swarmMail.appendEvent(event);
|
|
23
|
+
* const messages = await swarmMail.getInbox('agent-name', { limit: 5 });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { DatabaseAdapter } from "./types/database";
|
|
28
|
+
import type { SwarmMailAdapter } from "./types/adapter";
|
|
29
|
+
import type {
|
|
30
|
+
AgentRegisteredEvent,
|
|
31
|
+
MessageSentEvent,
|
|
32
|
+
FileReservedEvent,
|
|
33
|
+
} from "./streams/events";
|
|
34
|
+
|
|
35
|
+
// Import all implementation functions (now refactored to accept dbOverride)
|
|
36
|
+
import {
|
|
37
|
+
appendEvent,
|
|
38
|
+
appendEvents,
|
|
39
|
+
readEvents,
|
|
40
|
+
getLatestSequence,
|
|
41
|
+
replayEvents,
|
|
42
|
+
registerAgent,
|
|
43
|
+
sendMessage,
|
|
44
|
+
reserveFiles,
|
|
45
|
+
} from "./streams/store";
|
|
46
|
+
|
|
47
|
+
import {
|
|
48
|
+
getAgents,
|
|
49
|
+
getAgent,
|
|
50
|
+
getInbox,
|
|
51
|
+
getMessage,
|
|
52
|
+
getThreadMessages,
|
|
53
|
+
getActiveReservations,
|
|
54
|
+
checkConflicts,
|
|
55
|
+
} from "./streams/projections";
|
|
56
|
+
|
|
57
|
+
import { appendEvent as appendEventUtil } from "./streams/store";
|
|
58
|
+
import { createEvent } from "./streams/events";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a SwarmMailAdapter instance
|
|
62
|
+
*
|
|
63
|
+
* @param db - DatabaseAdapter instance (PGLite, SQLite, PostgreSQL, etc.)
|
|
64
|
+
* @param projectKey - Project identifier (typically the project path)
|
|
65
|
+
* @returns SwarmMailAdapter interface
|
|
66
|
+
*/
|
|
67
|
+
export function createSwarmMailAdapter(
|
|
68
|
+
db: DatabaseAdapter,
|
|
69
|
+
projectKey: string,
|
|
70
|
+
): SwarmMailAdapter {
|
|
71
|
+
return {
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Event Store Operations
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
async appendEvent(event, projectPath?) {
|
|
77
|
+
return appendEvent(event, projectPath, db);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async appendEvents(events, projectPath?) {
|
|
81
|
+
return appendEvents(events, projectPath, db);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async readEvents(options?, projectPath?) {
|
|
85
|
+
return readEvents(options, projectPath, db);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async getLatestSequence(projectKeyParam?, projectPath?) {
|
|
89
|
+
return getLatestSequence(projectKeyParam, projectPath, db);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async replayEvents(options?, projectPath?) {
|
|
93
|
+
return replayEvents(options, projectPath, db);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Agent Operations
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
async registerAgent(
|
|
101
|
+
projectKeyParam,
|
|
102
|
+
agentName,
|
|
103
|
+
options?,
|
|
104
|
+
projectPath?,
|
|
105
|
+
): Promise<AgentRegisteredEvent & { id: number; sequence: number }> {
|
|
106
|
+
return registerAgent(
|
|
107
|
+
projectKeyParam,
|
|
108
|
+
agentName,
|
|
109
|
+
options,
|
|
110
|
+
projectPath,
|
|
111
|
+
db,
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async getAgents(projectKeyParam, projectPath?) {
|
|
116
|
+
return getAgents(projectKeyParam, projectPath, db);
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
async getAgent(projectKeyParam, agentName, projectPath?) {
|
|
120
|
+
return getAgent(projectKeyParam, agentName, projectPath, db);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Messaging Operations
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
async sendMessage(
|
|
128
|
+
projectKeyParam,
|
|
129
|
+
fromAgent,
|
|
130
|
+
toAgents,
|
|
131
|
+
subject,
|
|
132
|
+
body,
|
|
133
|
+
options?,
|
|
134
|
+
projectPath?,
|
|
135
|
+
): Promise<MessageSentEvent & { id: number; sequence: number }> {
|
|
136
|
+
return sendMessage(
|
|
137
|
+
projectKeyParam,
|
|
138
|
+
fromAgent,
|
|
139
|
+
toAgents,
|
|
140
|
+
subject,
|
|
141
|
+
body,
|
|
142
|
+
options,
|
|
143
|
+
projectPath,
|
|
144
|
+
db,
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
async getInbox(projectKeyParam, agentName, options?, projectPath?) {
|
|
149
|
+
return getInbox(projectKeyParam, agentName, options, projectPath, db);
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
async getMessage(projectKeyParam, messageId, projectPath?) {
|
|
153
|
+
return getMessage(projectKeyParam, messageId, projectPath, db);
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
async getThreadMessages(projectKeyParam, threadId, projectPath?) {
|
|
157
|
+
return getThreadMessages(projectKeyParam, threadId, projectPath, db);
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
async markMessageAsRead(
|
|
161
|
+
projectKeyParam,
|
|
162
|
+
messageId,
|
|
163
|
+
agentName,
|
|
164
|
+
projectPath?,
|
|
165
|
+
) {
|
|
166
|
+
// Create message_read event
|
|
167
|
+
const event = createEvent("message_read", {
|
|
168
|
+
project_key: projectKeyParam,
|
|
169
|
+
message_id: messageId,
|
|
170
|
+
agent_name: agentName,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await appendEventUtil(event, projectPath, db);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async acknowledgeMessage(
|
|
177
|
+
projectKeyParam,
|
|
178
|
+
messageId,
|
|
179
|
+
agentName,
|
|
180
|
+
projectPath?,
|
|
181
|
+
) {
|
|
182
|
+
// Create message_acked event
|
|
183
|
+
const event = createEvent("message_acked", {
|
|
184
|
+
project_key: projectKeyParam,
|
|
185
|
+
message_id: messageId,
|
|
186
|
+
agent_name: agentName,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await appendEventUtil(event, projectPath, db);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// Reservation Operations
|
|
194
|
+
// ============================================================================
|
|
195
|
+
|
|
196
|
+
async reserveFiles(
|
|
197
|
+
projectKeyParam,
|
|
198
|
+
agentName,
|
|
199
|
+
paths,
|
|
200
|
+
options?,
|
|
201
|
+
projectPath?,
|
|
202
|
+
): Promise<FileReservedEvent & { id: number; sequence: number }> {
|
|
203
|
+
return reserveFiles(
|
|
204
|
+
projectKeyParam,
|
|
205
|
+
agentName,
|
|
206
|
+
paths,
|
|
207
|
+
options,
|
|
208
|
+
projectPath,
|
|
209
|
+
db,
|
|
210
|
+
);
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
async releaseFiles(projectKeyParam, agentName, options?, projectPath?) {
|
|
214
|
+
// Create file_released event
|
|
215
|
+
const event = createEvent("file_released", {
|
|
216
|
+
project_key: projectKeyParam,
|
|
217
|
+
agent_name: agentName,
|
|
218
|
+
paths: options?.paths,
|
|
219
|
+
reservation_ids: options?.reservationIds,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await appendEventUtil(event, projectPath, db);
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
async getActiveReservations(projectKeyParam, projectPath?, agentName?) {
|
|
226
|
+
return getActiveReservations(projectKeyParam, projectPath, agentName, db);
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
async checkConflicts(projectKeyParam, agentName, paths, projectPath?) {
|
|
230
|
+
return checkConflicts(projectKeyParam, agentName, paths, projectPath, db);
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
// ============================================================================
|
|
234
|
+
// Schema and Health Operations
|
|
235
|
+
// ============================================================================
|
|
236
|
+
|
|
237
|
+
async runMigrations(projectPath?) {
|
|
238
|
+
// Import migrations module and pass db
|
|
239
|
+
// Note: migrations expects PGlite but DatabaseAdapter is compatible
|
|
240
|
+
const { runMigrations: runMigrationsImpl } =
|
|
241
|
+
await import("./streams/migrations");
|
|
242
|
+
await runMigrationsImpl(db as any);
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
async healthCheck(projectPath?) {
|
|
246
|
+
// Simple query to check if db is working
|
|
247
|
+
try {
|
|
248
|
+
const result = await db.query("SELECT 1 as ok");
|
|
249
|
+
return result.rows.length > 0;
|
|
250
|
+
} catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
async getDatabaseStats(projectPath?) {
|
|
256
|
+
const [events, agents, messages, reservations] = await Promise.all([
|
|
257
|
+
db.query<{ count: string }>("SELECT COUNT(*) as count FROM events"),
|
|
258
|
+
db.query<{ count: string }>("SELECT COUNT(*) as count FROM agents"),
|
|
259
|
+
db.query<{ count: string }>("SELECT COUNT(*) as count FROM messages"),
|
|
260
|
+
db.query<{ count: string }>(
|
|
261
|
+
"SELECT COUNT(*) as count FROM reservations WHERE released_at IS NULL",
|
|
262
|
+
),
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
events: parseInt(events.rows[0]?.count || "0"),
|
|
267
|
+
agents: parseInt(agents.rows[0]?.count || "0"),
|
|
268
|
+
messages: parseInt(messages.rows[0]?.count || "0"),
|
|
269
|
+
reservations: parseInt(reservations.rows[0]?.count || "0"),
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
async resetDatabase(projectPath?) {
|
|
274
|
+
await db.exec(`
|
|
275
|
+
DELETE FROM message_recipients;
|
|
276
|
+
DELETE FROM messages;
|
|
277
|
+
DELETE FROM reservations;
|
|
278
|
+
DELETE FROM agents;
|
|
279
|
+
DELETE FROM events;
|
|
280
|
+
DELETE FROM locks;
|
|
281
|
+
DELETE FROM cursors;
|
|
282
|
+
`);
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
// ============================================================================
|
|
286
|
+
// Database Connection Management
|
|
287
|
+
// ============================================================================
|
|
288
|
+
|
|
289
|
+
async getDatabase(projectPath?) {
|
|
290
|
+
return db;
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
async close(projectPath?) {
|
|
294
|
+
if (db.close) {
|
|
295
|
+
await db.close();
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
async closeAll() {
|
|
300
|
+
// For single-instance adapter, same as close()
|
|
301
|
+
if (db.close) {
|
|
302
|
+
await db.close();
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm Mail - Actor-model primitives for multi-agent coordination
|
|
3
|
+
*
|
|
4
|
+
* ## Simple API (PGLite convenience layer)
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { getSwarmMail } from '@opencode/swarm-mail';
|
|
7
|
+
* const swarmMail = await getSwarmMail('/path/to/project');
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* ## Advanced API (database-agnostic adapter)
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { createSwarmMailAdapter } from '@opencode/swarm-mail';
|
|
13
|
+
* const db = createCustomDbAdapter({ path: './custom.db' });
|
|
14
|
+
* const swarmMail = createSwarmMailAdapter(db, '/path/to/project');
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const SWARM_MAIL_VERSION = "0.1.0";
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Core (database-agnostic)
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
export { createSwarmMailAdapter } from "./adapter";
|
|
25
|
+
export type {
|
|
26
|
+
DatabaseAdapter,
|
|
27
|
+
SwarmMailAdapter,
|
|
28
|
+
EventStoreAdapter,
|
|
29
|
+
AgentAdapter,
|
|
30
|
+
MessagingAdapter,
|
|
31
|
+
ReservationAdapter,
|
|
32
|
+
SchemaAdapter,
|
|
33
|
+
ReadEventsOptions,
|
|
34
|
+
InboxOptions,
|
|
35
|
+
Message,
|
|
36
|
+
Reservation,
|
|
37
|
+
Conflict,
|
|
38
|
+
} from "./types";
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// PGLite Convenience Layer
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
getSwarmMail,
|
|
46
|
+
createInMemorySwarmMail,
|
|
47
|
+
closeSwarmMail,
|
|
48
|
+
closeAllSwarmMail,
|
|
49
|
+
getDatabasePath,
|
|
50
|
+
PGlite,
|
|
51
|
+
} from "./pglite";
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Re-export everything from streams for backward compatibility
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
export * from "./streams";
|