opencode-swarm-plugin 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/.beads/.local_version +1 -0
- package/.beads/README.md +81 -0
- package/.beads/config.yaml +62 -0
- package/.beads/issues.jsonl +549 -0
- package/.beads/metadata.json +4 -0
- package/.gitattributes +3 -0
- package/Dockerfile +30 -0
- package/README.md +312 -0
- package/bun.lock +212 -0
- package/dist/index.js +14627 -0
- package/dist/plugin.js +14562 -0
- package/docker/agent-mail/Dockerfile +23 -0
- package/docker/agent-mail/__pycache__/server.cpython-314.pyc +0 -0
- package/docker/agent-mail/requirements.txt +3 -0
- package/docker/agent-mail/server.py +879 -0
- package/docker-compose.yml +45 -0
- package/package.json +52 -0
- package/scripts/docker-entrypoint.sh +54 -0
- package/src/agent-mail.integration.test.ts +1321 -0
- package/src/agent-mail.ts +665 -0
- package/src/anti-patterns.ts +430 -0
- package/src/beads.integration.test.ts +688 -0
- package/src/beads.ts +603 -0
- package/src/index.ts +267 -0
- package/src/learning.integration.test.ts +1104 -0
- package/src/learning.ts +438 -0
- package/src/pattern-maturity.ts +487 -0
- package/src/plugin.ts +11 -0
- package/src/schemas/bead.ts +152 -0
- package/src/schemas/evaluation.ts +133 -0
- package/src/schemas/index.test.ts +199 -0
- package/src/schemas/index.ts +77 -0
- package/src/schemas/task.ts +129 -0
- package/src/structured.ts +708 -0
- package/src/swarm.integration.test.ts +763 -0
- package/src/swarm.ts +1411 -0
- package/tsconfig.json +28 -0
- package/vitest.integration.config.ts +13 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Mail Module - MCP client for multi-agent coordination
|
|
3
|
+
*
|
|
4
|
+
* This module provides type-safe wrappers around the Agent Mail MCP server.
|
|
5
|
+
* It enforces context-preservation defaults to prevent session exhaustion.
|
|
6
|
+
*
|
|
7
|
+
* CRITICAL CONSTRAINTS:
|
|
8
|
+
* - fetch_inbox ALWAYS uses include_bodies: false
|
|
9
|
+
* - fetch_inbox ALWAYS limits to 5 messages max
|
|
10
|
+
* - Use summarize_thread instead of fetching all messages
|
|
11
|
+
* - Auto-release reservations when tasks complete
|
|
12
|
+
*/
|
|
13
|
+
import { tool } from "@opencode-ai/plugin";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Configuration
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const AGENT_MAIL_URL = "http://127.0.0.1:8765";
|
|
21
|
+
const DEFAULT_TTL_SECONDS = 3600; // 1 hour
|
|
22
|
+
const MAX_INBOX_LIMIT = 5; // HARD CAP - never exceed this
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Types
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/** Agent Mail session state */
|
|
29
|
+
export interface AgentMailState {
|
|
30
|
+
projectKey: string;
|
|
31
|
+
agentName: string;
|
|
32
|
+
reservations: number[];
|
|
33
|
+
startedAt: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Module-level state (keyed by sessionID)
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* State storage keyed by sessionID.
|
|
42
|
+
* Since ToolContext doesn't have persistent state, we use a module-level map.
|
|
43
|
+
*/
|
|
44
|
+
const sessionStates = new Map<string, AgentMailState>();
|
|
45
|
+
|
|
46
|
+
/** MCP JSON-RPC response */
|
|
47
|
+
interface MCPResponse<T = unknown> {
|
|
48
|
+
jsonrpc: "2.0";
|
|
49
|
+
id: string;
|
|
50
|
+
result?: T;
|
|
51
|
+
error?: {
|
|
52
|
+
code: number;
|
|
53
|
+
message: string;
|
|
54
|
+
data?: unknown;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Agent registration result */
|
|
59
|
+
interface AgentInfo {
|
|
60
|
+
id: number;
|
|
61
|
+
name: string;
|
|
62
|
+
program: string;
|
|
63
|
+
model: string;
|
|
64
|
+
task_description: string;
|
|
65
|
+
inception_ts: string;
|
|
66
|
+
last_active_ts: string;
|
|
67
|
+
project_id: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Project info */
|
|
71
|
+
interface ProjectInfo {
|
|
72
|
+
id: number;
|
|
73
|
+
slug: string;
|
|
74
|
+
human_key: string;
|
|
75
|
+
created_at: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Message header (no body) */
|
|
79
|
+
interface MessageHeader {
|
|
80
|
+
id: number;
|
|
81
|
+
subject: string;
|
|
82
|
+
from: string;
|
|
83
|
+
created_ts: string;
|
|
84
|
+
importance: string;
|
|
85
|
+
ack_required: boolean;
|
|
86
|
+
thread_id?: string;
|
|
87
|
+
kind?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** File reservation result */
|
|
91
|
+
interface ReservationResult {
|
|
92
|
+
granted: Array<{
|
|
93
|
+
id: number;
|
|
94
|
+
path_pattern: string;
|
|
95
|
+
exclusive: boolean;
|
|
96
|
+
reason: string;
|
|
97
|
+
expires_ts: string;
|
|
98
|
+
}>;
|
|
99
|
+
conflicts: Array<{
|
|
100
|
+
path: string;
|
|
101
|
+
holders: string[];
|
|
102
|
+
}>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Thread summary */
|
|
106
|
+
interface ThreadSummary {
|
|
107
|
+
thread_id: string;
|
|
108
|
+
summary: {
|
|
109
|
+
participants: string[];
|
|
110
|
+
key_points: string[];
|
|
111
|
+
action_items: string[];
|
|
112
|
+
total_messages: number;
|
|
113
|
+
};
|
|
114
|
+
examples?: Array<{
|
|
115
|
+
id: number;
|
|
116
|
+
subject: string;
|
|
117
|
+
from: string;
|
|
118
|
+
body_md?: string;
|
|
119
|
+
}>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// Errors
|
|
124
|
+
// ============================================================================
|
|
125
|
+
|
|
126
|
+
export class AgentMailError extends Error {
|
|
127
|
+
constructor(
|
|
128
|
+
message: string,
|
|
129
|
+
public readonly tool: string,
|
|
130
|
+
public readonly code?: number,
|
|
131
|
+
public readonly data?: unknown,
|
|
132
|
+
) {
|
|
133
|
+
super(message);
|
|
134
|
+
this.name = "AgentMailError";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class AgentMailNotInitializedError extends Error {
|
|
139
|
+
constructor() {
|
|
140
|
+
super("Agent Mail not initialized. Call agent-mail:init first.");
|
|
141
|
+
this.name = "AgentMailNotInitializedError";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export class FileReservationConflictError extends Error {
|
|
146
|
+
constructor(
|
|
147
|
+
message: string,
|
|
148
|
+
public readonly conflicts: Array<{ path: string; holders: string[] }>,
|
|
149
|
+
) {
|
|
150
|
+
super(message);
|
|
151
|
+
this.name = "FileReservationConflictError";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// MCP Client
|
|
157
|
+
// ============================================================================
|
|
158
|
+
|
|
159
|
+
/** MCP tool result with content wrapper (real Agent Mail format) */
|
|
160
|
+
interface MCPToolResult<T = unknown> {
|
|
161
|
+
content?: Array<{ type: string; text: string }>;
|
|
162
|
+
structuredContent?: T;
|
|
163
|
+
isError?: boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Call an Agent Mail MCP tool
|
|
168
|
+
*
|
|
169
|
+
* Handles both direct results (mock server) and wrapped results (real server).
|
|
170
|
+
* Real Agent Mail returns: { content: [...], structuredContent: {...} }
|
|
171
|
+
*/
|
|
172
|
+
async function mcpCall<T>(
|
|
173
|
+
toolName: string,
|
|
174
|
+
args: Record<string, unknown>,
|
|
175
|
+
): Promise<T> {
|
|
176
|
+
const response = await fetch(`${AGENT_MAIL_URL}/mcp/`, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: { "Content-Type": "application/json" },
|
|
179
|
+
body: JSON.stringify({
|
|
180
|
+
jsonrpc: "2.0",
|
|
181
|
+
id: crypto.randomUUID(),
|
|
182
|
+
method: "tools/call",
|
|
183
|
+
params: { name: toolName, arguments: args },
|
|
184
|
+
}),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
throw new AgentMailError(
|
|
189
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
190
|
+
toolName,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const json = (await response.json()) as MCPResponse<MCPToolResult<T> | T>;
|
|
195
|
+
|
|
196
|
+
if (json.error) {
|
|
197
|
+
throw new AgentMailError(
|
|
198
|
+
json.error.message,
|
|
199
|
+
toolName,
|
|
200
|
+
json.error.code,
|
|
201
|
+
json.error.data,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const result = json.result;
|
|
206
|
+
|
|
207
|
+
// Handle wrapped response format (real Agent Mail server)
|
|
208
|
+
// Check for isError first (error responses don't have structuredContent)
|
|
209
|
+
if (result && typeof result === "object") {
|
|
210
|
+
const wrapped = result as MCPToolResult<T>;
|
|
211
|
+
|
|
212
|
+
// Check for error response (has isError: true but no structuredContent)
|
|
213
|
+
if (wrapped.isError) {
|
|
214
|
+
const errorText = wrapped.content?.[0]?.text || "Unknown error";
|
|
215
|
+
throw new AgentMailError(errorText, toolName);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check for success response with structuredContent
|
|
219
|
+
if ("structuredContent" in wrapped) {
|
|
220
|
+
return wrapped.structuredContent as T;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Handle direct response format (mock server)
|
|
225
|
+
return result as T;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get Agent Mail state for a session, or throw if not initialized
|
|
230
|
+
*/
|
|
231
|
+
function requireState(sessionID: string): AgentMailState {
|
|
232
|
+
const state = sessionStates.get(sessionID);
|
|
233
|
+
if (!state) {
|
|
234
|
+
throw new AgentMailNotInitializedError();
|
|
235
|
+
}
|
|
236
|
+
return state;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Store Agent Mail state for a session
|
|
241
|
+
*/
|
|
242
|
+
function setState(sessionID: string, state: AgentMailState): void {
|
|
243
|
+
sessionStates.set(sessionID, state);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get state if exists (for cleanup hooks)
|
|
248
|
+
*/
|
|
249
|
+
function getState(sessionID: string): AgentMailState | undefined {
|
|
250
|
+
return sessionStates.get(sessionID);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Clear state for a session
|
|
255
|
+
*/
|
|
256
|
+
function clearState(sessionID: string): void {
|
|
257
|
+
sessionStates.delete(sessionID);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// Tool Definitions
|
|
262
|
+
// ============================================================================
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Initialize Agent Mail session
|
|
266
|
+
*/
|
|
267
|
+
export const agentmail_init = tool({
|
|
268
|
+
description:
|
|
269
|
+
"Initialize Agent Mail session (ensure project + register agent)",
|
|
270
|
+
args: {
|
|
271
|
+
project_path: tool.schema
|
|
272
|
+
.string()
|
|
273
|
+
.describe("Absolute path to the project/repo"),
|
|
274
|
+
agent_name: tool.schema
|
|
275
|
+
.string()
|
|
276
|
+
.optional()
|
|
277
|
+
.describe("Agent name (omit for auto-generated adjective+noun)"),
|
|
278
|
+
task_description: tool.schema
|
|
279
|
+
.string()
|
|
280
|
+
.optional()
|
|
281
|
+
.describe("Description of current task"),
|
|
282
|
+
},
|
|
283
|
+
async execute(args, ctx) {
|
|
284
|
+
// 1. Ensure project exists
|
|
285
|
+
const project = await mcpCall<ProjectInfo>("ensure_project", {
|
|
286
|
+
human_key: args.project_path,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// 2. Register agent
|
|
290
|
+
const agent = await mcpCall<AgentInfo>("register_agent", {
|
|
291
|
+
project_key: args.project_path,
|
|
292
|
+
program: "opencode",
|
|
293
|
+
model: "claude-opus-4",
|
|
294
|
+
name: args.agent_name, // undefined = auto-generate
|
|
295
|
+
task_description: args.task_description || "",
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// 3. Store state using sessionID
|
|
299
|
+
const state: AgentMailState = {
|
|
300
|
+
projectKey: args.project_path,
|
|
301
|
+
agentName: agent.name,
|
|
302
|
+
reservations: [],
|
|
303
|
+
startedAt: new Date().toISOString(),
|
|
304
|
+
};
|
|
305
|
+
setState(ctx.sessionID, state);
|
|
306
|
+
|
|
307
|
+
return JSON.stringify({ project, agent }, null, 2);
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Send a message to other agents
|
|
313
|
+
*/
|
|
314
|
+
export const agentmail_send = tool({
|
|
315
|
+
description: "Send message to other agents",
|
|
316
|
+
args: {
|
|
317
|
+
to: tool.schema
|
|
318
|
+
.array(tool.schema.string())
|
|
319
|
+
.describe("Recipient agent names"),
|
|
320
|
+
subject: tool.schema.string().describe("Message subject"),
|
|
321
|
+
body: tool.schema.string().describe("Message body (Markdown)"),
|
|
322
|
+
thread_id: tool.schema
|
|
323
|
+
.string()
|
|
324
|
+
.optional()
|
|
325
|
+
.describe("Thread ID (use bead ID for linking)"),
|
|
326
|
+
importance: tool.schema
|
|
327
|
+
.enum(["low", "normal", "high", "urgent"])
|
|
328
|
+
.optional()
|
|
329
|
+
.describe("Message importance (default: normal)"),
|
|
330
|
+
ack_required: tool.schema
|
|
331
|
+
.boolean()
|
|
332
|
+
.optional()
|
|
333
|
+
.describe("Require acknowledgement (default: false)"),
|
|
334
|
+
},
|
|
335
|
+
async execute(args, ctx) {
|
|
336
|
+
const state = requireState(ctx.sessionID);
|
|
337
|
+
|
|
338
|
+
await mcpCall("send_message", {
|
|
339
|
+
project_key: state.projectKey,
|
|
340
|
+
sender_name: state.agentName,
|
|
341
|
+
to: args.to,
|
|
342
|
+
subject: args.subject,
|
|
343
|
+
body_md: args.body,
|
|
344
|
+
thread_id: args.thread_id,
|
|
345
|
+
importance: args.importance || "normal",
|
|
346
|
+
ack_required: args.ack_required || false,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return `Message sent to ${args.to.join(", ")}`;
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Fetch inbox (CONTEXT-SAFE: bodies excluded, limit 5)
|
|
355
|
+
*/
|
|
356
|
+
export const agentmail_inbox = tool({
|
|
357
|
+
description: "Fetch inbox (CONTEXT-SAFE: bodies excluded, limit 5)",
|
|
358
|
+
args: {
|
|
359
|
+
limit: tool.schema
|
|
360
|
+
.number()
|
|
361
|
+
.max(MAX_INBOX_LIMIT)
|
|
362
|
+
.optional()
|
|
363
|
+
.describe(`Max messages (hard cap: ${MAX_INBOX_LIMIT})`),
|
|
364
|
+
urgent_only: tool.schema
|
|
365
|
+
.boolean()
|
|
366
|
+
.optional()
|
|
367
|
+
.describe("Only show urgent messages"),
|
|
368
|
+
since_ts: tool.schema
|
|
369
|
+
.string()
|
|
370
|
+
.optional()
|
|
371
|
+
.describe("Only messages after this ISO-8601 timestamp"),
|
|
372
|
+
},
|
|
373
|
+
async execute(args, ctx) {
|
|
374
|
+
const state = requireState(ctx.sessionID);
|
|
375
|
+
|
|
376
|
+
// CRITICAL: Enforce context-safe defaults
|
|
377
|
+
const limit = Math.min(args.limit || MAX_INBOX_LIMIT, MAX_INBOX_LIMIT);
|
|
378
|
+
|
|
379
|
+
const messages = await mcpCall<MessageHeader[]>("fetch_inbox", {
|
|
380
|
+
project_key: state.projectKey,
|
|
381
|
+
agent_name: state.agentName,
|
|
382
|
+
limit,
|
|
383
|
+
include_bodies: false, // MANDATORY - never include bodies
|
|
384
|
+
urgent_only: args.urgent_only || false,
|
|
385
|
+
since_ts: args.since_ts,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
return JSON.stringify(messages, null, 2);
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Read a single message body by ID
|
|
394
|
+
*/
|
|
395
|
+
export const agentmail_read_message = tool({
|
|
396
|
+
description: "Fetch ONE message body by ID (use after inbox)",
|
|
397
|
+
args: {
|
|
398
|
+
message_id: tool.schema.number().describe("Message ID from inbox"),
|
|
399
|
+
},
|
|
400
|
+
async execute(args, ctx) {
|
|
401
|
+
const state = requireState(ctx.sessionID);
|
|
402
|
+
|
|
403
|
+
// Mark as read
|
|
404
|
+
await mcpCall("mark_message_read", {
|
|
405
|
+
project_key: state.projectKey,
|
|
406
|
+
agent_name: state.agentName,
|
|
407
|
+
message_id: args.message_id,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Fetch with body - we need to use fetch_inbox with specific message
|
|
411
|
+
// Since there's no get_message, we'll use search
|
|
412
|
+
const messages = await mcpCall<MessageHeader[]>("fetch_inbox", {
|
|
413
|
+
project_key: state.projectKey,
|
|
414
|
+
agent_name: state.agentName,
|
|
415
|
+
limit: 1,
|
|
416
|
+
include_bodies: true, // Only for single message fetch
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const message = messages.find((m) => m.id === args.message_id);
|
|
420
|
+
if (!message) {
|
|
421
|
+
return `Message ${args.message_id} not found`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return JSON.stringify(message, null, 2);
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Summarize a thread (PREFERRED over fetching all messages)
|
|
430
|
+
*/
|
|
431
|
+
export const agentmail_summarize_thread = tool({
|
|
432
|
+
description: "Summarize thread (PREFERRED over fetching all messages)",
|
|
433
|
+
args: {
|
|
434
|
+
thread_id: tool.schema.string().describe("Thread ID (usually bead ID)"),
|
|
435
|
+
include_examples: tool.schema
|
|
436
|
+
.boolean()
|
|
437
|
+
.optional()
|
|
438
|
+
.describe("Include up to 3 sample messages"),
|
|
439
|
+
},
|
|
440
|
+
async execute(args, ctx) {
|
|
441
|
+
const state = requireState(ctx.sessionID);
|
|
442
|
+
|
|
443
|
+
const summary = await mcpCall<ThreadSummary>("summarize_thread", {
|
|
444
|
+
project_key: state.projectKey,
|
|
445
|
+
thread_id: args.thread_id,
|
|
446
|
+
include_examples: args.include_examples || false,
|
|
447
|
+
llm_mode: true, // Use LLM for better summaries
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
return JSON.stringify(summary, null, 2);
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Reserve file paths for exclusive editing
|
|
456
|
+
*/
|
|
457
|
+
export const agentmail_reserve = tool({
|
|
458
|
+
description: "Reserve file paths for exclusive editing",
|
|
459
|
+
args: {
|
|
460
|
+
paths: tool.schema
|
|
461
|
+
.array(tool.schema.string())
|
|
462
|
+
.describe("File paths or globs to reserve (e.g., src/auth/**)"),
|
|
463
|
+
ttl_seconds: tool.schema
|
|
464
|
+
.number()
|
|
465
|
+
.optional()
|
|
466
|
+
.describe(`Time to live in seconds (default: ${DEFAULT_TTL_SECONDS})`),
|
|
467
|
+
exclusive: tool.schema
|
|
468
|
+
.boolean()
|
|
469
|
+
.optional()
|
|
470
|
+
.describe("Exclusive lock (default: true)"),
|
|
471
|
+
reason: tool.schema
|
|
472
|
+
.string()
|
|
473
|
+
.optional()
|
|
474
|
+
.describe("Reason for reservation (include bead ID)"),
|
|
475
|
+
},
|
|
476
|
+
async execute(args, ctx) {
|
|
477
|
+
const state = requireState(ctx.sessionID);
|
|
478
|
+
|
|
479
|
+
const result = await mcpCall<ReservationResult>("file_reservation_paths", {
|
|
480
|
+
project_key: state.projectKey,
|
|
481
|
+
agent_name: state.agentName,
|
|
482
|
+
paths: args.paths,
|
|
483
|
+
ttl_seconds: args.ttl_seconds || DEFAULT_TTL_SECONDS,
|
|
484
|
+
exclusive: args.exclusive ?? true,
|
|
485
|
+
reason: args.reason || "",
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Handle unexpected response structure
|
|
489
|
+
if (!result) {
|
|
490
|
+
throw new AgentMailError(
|
|
491
|
+
"Unexpected response: file_reservation_paths returned null/undefined",
|
|
492
|
+
"file_reservation_paths",
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Check for conflicts
|
|
497
|
+
if (result.conflicts && result.conflicts.length > 0) {
|
|
498
|
+
const conflictDetails = result.conflicts
|
|
499
|
+
.map((c) => `${c.path}: held by ${c.holders.join(", ")}`)
|
|
500
|
+
.join("\n");
|
|
501
|
+
|
|
502
|
+
throw new FileReservationConflictError(
|
|
503
|
+
`Cannot reserve files:\n${conflictDetails}`,
|
|
504
|
+
result.conflicts,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Handle case where granted is undefined/null (alternative response formats)
|
|
509
|
+
const granted = result.granted ?? [];
|
|
510
|
+
if (!Array.isArray(granted)) {
|
|
511
|
+
throw new AgentMailError(
|
|
512
|
+
`Unexpected response format: expected granted to be an array, got ${typeof granted}`,
|
|
513
|
+
"file_reservation_paths",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Store reservation IDs for auto-release
|
|
518
|
+
const reservationIds = granted.map((r) => r.id);
|
|
519
|
+
state.reservations = [...state.reservations, ...reservationIds];
|
|
520
|
+
setState(ctx.sessionID, state);
|
|
521
|
+
|
|
522
|
+
if (granted.length === 0) {
|
|
523
|
+
return "No paths were reserved (empty granted list)";
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return `Reserved ${granted.length} path(s):\n${granted
|
|
527
|
+
.map((r) => ` - ${r.path_pattern} (expires: ${r.expires_ts})`)
|
|
528
|
+
.join("\n")}`;
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Release file reservations
|
|
534
|
+
*/
|
|
535
|
+
export const agentmail_release = tool({
|
|
536
|
+
description: "Release file reservations (auto-called on task completion)",
|
|
537
|
+
args: {
|
|
538
|
+
paths: tool.schema
|
|
539
|
+
.array(tool.schema.string())
|
|
540
|
+
.optional()
|
|
541
|
+
.describe("Specific paths to release (omit for all)"),
|
|
542
|
+
reservation_ids: tool.schema
|
|
543
|
+
.array(tool.schema.number())
|
|
544
|
+
.optional()
|
|
545
|
+
.describe("Specific reservation IDs to release"),
|
|
546
|
+
},
|
|
547
|
+
async execute(args, ctx) {
|
|
548
|
+
const state = requireState(ctx.sessionID);
|
|
549
|
+
|
|
550
|
+
const result = await mcpCall<{ released: number; released_at: string }>(
|
|
551
|
+
"release_file_reservations",
|
|
552
|
+
{
|
|
553
|
+
project_key: state.projectKey,
|
|
554
|
+
agent_name: state.agentName,
|
|
555
|
+
paths: args.paths,
|
|
556
|
+
file_reservation_ids: args.reservation_ids,
|
|
557
|
+
},
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
// Clear stored reservation IDs
|
|
561
|
+
state.reservations = [];
|
|
562
|
+
setState(ctx.sessionID, state);
|
|
563
|
+
|
|
564
|
+
return `Released ${result.released} reservation(s)`;
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Acknowledge a message
|
|
570
|
+
*/
|
|
571
|
+
export const agentmail_ack = tool({
|
|
572
|
+
description: "Acknowledge a message (for ack_required messages)",
|
|
573
|
+
args: {
|
|
574
|
+
message_id: tool.schema.number().describe("Message ID to acknowledge"),
|
|
575
|
+
},
|
|
576
|
+
async execute(args, ctx) {
|
|
577
|
+
const state = requireState(ctx.sessionID);
|
|
578
|
+
|
|
579
|
+
await mcpCall("acknowledge_message", {
|
|
580
|
+
project_key: state.projectKey,
|
|
581
|
+
agent_name: state.agentName,
|
|
582
|
+
message_id: args.message_id,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
return `Acknowledged message ${args.message_id}`;
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Search messages
|
|
591
|
+
*/
|
|
592
|
+
export const agentmail_search = tool({
|
|
593
|
+
description: "Search messages by keyword (FTS5 syntax supported)",
|
|
594
|
+
args: {
|
|
595
|
+
query: tool.schema
|
|
596
|
+
.string()
|
|
597
|
+
.describe('Search query (e.g., "build plan", plan AND users)'),
|
|
598
|
+
limit: tool.schema
|
|
599
|
+
.number()
|
|
600
|
+
.optional()
|
|
601
|
+
.describe("Max results (default: 20)"),
|
|
602
|
+
},
|
|
603
|
+
async execute(args, ctx) {
|
|
604
|
+
const state = requireState(ctx.sessionID);
|
|
605
|
+
|
|
606
|
+
const results = await mcpCall<MessageHeader[]>("search_messages", {
|
|
607
|
+
project_key: state.projectKey,
|
|
608
|
+
query: args.query,
|
|
609
|
+
limit: args.limit || 20,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
return JSON.stringify(results, null, 2);
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Check Agent Mail health
|
|
618
|
+
*/
|
|
619
|
+
export const agentmail_health = tool({
|
|
620
|
+
description: "Check if Agent Mail server is running",
|
|
621
|
+
args: {},
|
|
622
|
+
async execute(args, ctx) {
|
|
623
|
+
try {
|
|
624
|
+
const response = await fetch(`${AGENT_MAIL_URL}/health/liveness`);
|
|
625
|
+
if (response.ok) {
|
|
626
|
+
return "Agent Mail is running";
|
|
627
|
+
}
|
|
628
|
+
return `Agent Mail returned status ${response.status}`;
|
|
629
|
+
} catch (error) {
|
|
630
|
+
return `Agent Mail not reachable: ${error instanceof Error ? error.message : String(error)}`;
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// ============================================================================
|
|
636
|
+
// Export all tools
|
|
637
|
+
// ============================================================================
|
|
638
|
+
|
|
639
|
+
export const agentMailTools = {
|
|
640
|
+
agentmail_init: agentmail_init,
|
|
641
|
+
agentmail_send: agentmail_send,
|
|
642
|
+
agentmail_inbox: agentmail_inbox,
|
|
643
|
+
agentmail_read_message: agentmail_read_message,
|
|
644
|
+
agentmail_summarize_thread: agentmail_summarize_thread,
|
|
645
|
+
agentmail_reserve: agentmail_reserve,
|
|
646
|
+
agentmail_release: agentmail_release,
|
|
647
|
+
agentmail_ack: agentmail_ack,
|
|
648
|
+
agentmail_search: agentmail_search,
|
|
649
|
+
agentmail_health: agentmail_health,
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// ============================================================================
|
|
653
|
+
// Utility exports for other modules
|
|
654
|
+
// ============================================================================
|
|
655
|
+
|
|
656
|
+
export {
|
|
657
|
+
mcpCall,
|
|
658
|
+
requireState,
|
|
659
|
+
setState,
|
|
660
|
+
getState,
|
|
661
|
+
clearState,
|
|
662
|
+
sessionStates,
|
|
663
|
+
AGENT_MAIL_URL,
|
|
664
|
+
MAX_INBOX_LIMIT,
|
|
665
|
+
};
|