@intx/tools-mail 0.1.2 → 0.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.
@@ -0,0 +1,329 @@
1
+ // Per-tool handler factories for the five mail tools. Each factory takes
2
+ // the bound MessageTransport and returns a closed-over ToolHandler.
3
+ //
4
+ // Keeping the factories at this granularity (one per tool, pure
5
+ // (MessageTransport) → ToolHandler) is deliberate: the package's
6
+ // public surface in index.ts resolves the transport once at handler-init
7
+ // and wires the handlers in a single place. The factories themselves
8
+ // carry no resolver vocabulary, so a future per-tool composition can
9
+ // reuse them unchanged.
10
+ //
11
+ // (MESSAGE.md § Mail Tools)
12
+ import { type } from "arktype";
13
+ import { InterchangeType } from "@intx/types/runtime";
14
+ // ---------------------------------------------------------------------------
15
+ // Argument schemas
16
+ // ---------------------------------------------------------------------------
17
+ const SendArgs = type({
18
+ to: "string | string[]",
19
+ "type?": InterchangeType,
20
+ "content?": "string",
21
+ "payload?": "Record<string, unknown>",
22
+ "subject?": "string",
23
+ "inReplyTo?": "string",
24
+ });
25
+ const ReplyArgs = type({
26
+ ref: { uid: "number", mailbox: "string" },
27
+ "type?": InterchangeType,
28
+ "content?": "string",
29
+ "payload?": "Record<string, unknown>",
30
+ });
31
+ const SearchArgs = type({
32
+ "mailbox?": "string",
33
+ "query?": "Record<string, unknown>",
34
+ "limit?": "number",
35
+ });
36
+ const ReadArgs = type({
37
+ ref: { uid: "number", mailbox: "string" },
38
+ "parts?": "string",
39
+ });
40
+ const WaitArgs = type({
41
+ "query?": "Record<string, unknown>",
42
+ "timeout?": "number",
43
+ "mailbox?": "string",
44
+ });
45
+ // ---------------------------------------------------------------------------
46
+ // Individual tool handlers
47
+ // ---------------------------------------------------------------------------
48
+ export function makeMailSendHandler(transport) {
49
+ return async (call, signal) => {
50
+ const args = SendArgs(call.arguments);
51
+ if (args instanceof type.errors) {
52
+ return errorResult(call.id, args.summary);
53
+ }
54
+ const { content, payload } = args;
55
+ if (content !== undefined && payload !== undefined) {
56
+ return errorResult(call.id, "provide either 'content' or 'payload', not both");
57
+ }
58
+ const outbound = {
59
+ to: args.to,
60
+ type: args.type ?? "conversation.message",
61
+ };
62
+ if (args.subject !== undefined) {
63
+ outbound.subject = args.subject;
64
+ }
65
+ if (content !== undefined) {
66
+ outbound.content = content;
67
+ }
68
+ if (payload !== undefined) {
69
+ outbound.payload = payload;
70
+ }
71
+ if (args.inReplyTo !== undefined) {
72
+ outbound.inReplyTo = args.inReplyTo;
73
+ }
74
+ let receipt;
75
+ try {
76
+ receipt = await transport.send(outbound, signal);
77
+ }
78
+ catch (cause) {
79
+ return errorResult(call.id, `send_failed: ${cause instanceof Error ? cause.message : String(cause)}`, "send_failed");
80
+ }
81
+ return { callId: call.id, content: { messageId: receipt.messageId } };
82
+ };
83
+ }
84
+ export function makeMailReplyHandler(transport) {
85
+ return async (call, signal) => {
86
+ const args = ReplyArgs(call.arguments);
87
+ if (args instanceof type.errors) {
88
+ return errorResult(call.id, args.summary);
89
+ }
90
+ const messageRef = args.ref;
91
+ // Fetch the parent message to retrieve threading headers.
92
+ let parentHeaders;
93
+ try {
94
+ parentHeaders = await transport.fetchHeaders(messageRef, signal);
95
+ }
96
+ catch (cause) {
97
+ return errorResult(call.id, `failed to fetch parent message: ${cause instanceof Error ? cause.message : String(cause)}`);
98
+ }
99
+ const { content, payload } = args;
100
+ if (content !== undefined && payload !== undefined) {
101
+ return errorResult(call.id, "provide either 'content' or 'payload', not both");
102
+ }
103
+ const outbound = {
104
+ to: parentHeaders.from,
105
+ type: args.type ?? "conversation.message",
106
+ inReplyTo: parentHeaders.messageId,
107
+ };
108
+ // Carry forward the subject if available.
109
+ if (parentHeaders.subject !== undefined) {
110
+ outbound.subject = parentHeaders.subject;
111
+ }
112
+ if (content !== undefined) {
113
+ outbound.content = content;
114
+ }
115
+ if (payload !== undefined) {
116
+ outbound.payload = payload;
117
+ }
118
+ // OutboundMessage does not carry a References field; the transport
119
+ // builds the threading chain from inReplyTo when delivering the
120
+ // reply.
121
+ let receipt;
122
+ try {
123
+ receipt = await transport.send(outbound, signal);
124
+ }
125
+ catch (cause) {
126
+ return errorResult(call.id, `send_failed: ${cause instanceof Error ? cause.message : String(cause)}`, "send_failed");
127
+ }
128
+ return { callId: call.id, content: { messageId: receipt.messageId } };
129
+ };
130
+ }
131
+ export function makeMailSearchHandler(transport) {
132
+ return async (call, signal) => {
133
+ const args = SearchArgs(call.arguments);
134
+ if (args instanceof type.errors) {
135
+ return errorResult(call.id, args.summary);
136
+ }
137
+ const mailbox = args.mailbox ?? "INBOX";
138
+ const query = args.query ?? {};
139
+ const limit = args.limit ?? 20;
140
+ let refs;
141
+ try {
142
+ refs = await transport.search(mailbox, query, signal);
143
+ }
144
+ catch (cause) {
145
+ const msg = cause instanceof Error ? cause.message : String(cause);
146
+ const code = msg.includes("does not exist")
147
+ ? "invalid_mailbox"
148
+ : "invalid_query";
149
+ return errorResult(call.id, msg, code);
150
+ }
151
+ const limited = refs.slice(0, limit);
152
+ // Fetch summary headers for each result.
153
+ const summaries = await Promise.all(limited.map(async (ref) => {
154
+ try {
155
+ const headers = await transport.fetchHeaders(ref, signal);
156
+ return {
157
+ ref,
158
+ from: headers.from,
159
+ subject: headers.subject,
160
+ date: headers.date,
161
+ interchangeType: headers.interchangeType,
162
+ messageId: headers.messageId,
163
+ };
164
+ }
165
+ catch {
166
+ return { ref };
167
+ }
168
+ }));
169
+ return { callId: call.id, content: { results: summaries } };
170
+ };
171
+ }
172
+ export function makeMailReadHandler(transport) {
173
+ return async (call, signal) => {
174
+ const args = ReadArgs(call.arguments);
175
+ if (args instanceof type.errors) {
176
+ return errorResult(call.id, args.summary);
177
+ }
178
+ const messageRef = args.ref;
179
+ const parts = args.parts ?? "payload";
180
+ if (parts === "headers") {
181
+ let headers;
182
+ try {
183
+ headers = await transport.fetchHeaders(messageRef, signal);
184
+ }
185
+ catch (cause) {
186
+ return errorResult(call.id, `not_found: ${cause instanceof Error ? cause.message : String(cause)}`, "not_found");
187
+ }
188
+ return { callId: call.id, content: { headers } };
189
+ }
190
+ if (parts === "full") {
191
+ let message;
192
+ try {
193
+ message = await transport.fetchFull(messageRef, signal);
194
+ }
195
+ catch (cause) {
196
+ return errorResult(call.id, `not_found: ${cause instanceof Error ? cause.message : String(cause)}`, "not_found");
197
+ }
198
+ return {
199
+ callId: call.id,
200
+ content: {
201
+ headers: message.headers,
202
+ content: message.content,
203
+ payload: message.payload,
204
+ signatureStatus: message.signatureStatus,
205
+ flags: message.flags,
206
+ },
207
+ };
208
+ }
209
+ if (parts === "payload") {
210
+ let message;
211
+ try {
212
+ message = await transport.fetchFull(messageRef, signal);
213
+ }
214
+ catch (cause) {
215
+ return errorResult(call.id, `not_found: ${cause instanceof Error ? cause.message : String(cause)}`, "not_found");
216
+ }
217
+ if (message.payload !== undefined) {
218
+ return { callId: call.id, content: { payload: message.payload } };
219
+ }
220
+ // Conversation message — return content field.
221
+ return {
222
+ callId: call.id,
223
+ content: {
224
+ content: message.content,
225
+ interchangeType: message.headers.interchangeType,
226
+ },
227
+ };
228
+ }
229
+ // Specific MIME part path (e.g. "1.3").
230
+ let part;
231
+ try {
232
+ part = await transport.fetchPart(messageRef, parts, signal);
233
+ }
234
+ catch (cause) {
235
+ return errorResult(call.id, `invalid_part: ${cause instanceof Error ? cause.message : String(cause)}`, "invalid_part");
236
+ }
237
+ return {
238
+ callId: call.id,
239
+ content: {
240
+ contentType: part.contentType,
241
+ encoding: part.encoding,
242
+ content: new TextDecoder().decode(part.content),
243
+ },
244
+ };
245
+ };
246
+ }
247
+ export function makeMailWaitHandler(transport) {
248
+ return async (call, signal) => {
249
+ const args = WaitArgs(call.arguments);
250
+ if (args instanceof type.errors) {
251
+ return errorResult(call.id, args.summary);
252
+ }
253
+ const query = args.query ?? {};
254
+ const timeoutSeconds = args.timeout ?? 120;
255
+ const mailbox = args.mailbox ?? "INBOX";
256
+ // Check for an existing match first.
257
+ const existing = await transport.search(mailbox, query, signal);
258
+ const firstMatch = existing[0];
259
+ if (firstMatch !== undefined) {
260
+ const message = await transport.fetchFull(firstMatch, signal);
261
+ return {
262
+ callId: call.id,
263
+ content: {
264
+ ref: firstMatch,
265
+ from: message.headers.from,
266
+ subject: message.headers.subject,
267
+ content: message.content,
268
+ },
269
+ };
270
+ }
271
+ // No match yet — watch for new arrivals.
272
+ return new Promise((resolve) => {
273
+ let settled = false;
274
+ const unsubscribe = transport.watch(mailbox, (event) => {
275
+ if (settled)
276
+ return;
277
+ if (event.type !== "exists")
278
+ return;
279
+ // Match against the query's 'from' field (the primary use case).
280
+ if (typeof query.from === "string" &&
281
+ event.headers.from !== query.from) {
282
+ return;
283
+ }
284
+ settled = true;
285
+ unsubscribe();
286
+ clearTimeout(timer);
287
+ void (async () => {
288
+ const ref = { uid: event.uid, mailbox };
289
+ const message = await transport.fetchFull(ref, signal);
290
+ resolve({
291
+ callId: call.id,
292
+ content: {
293
+ ref,
294
+ from: message.headers.from,
295
+ subject: message.headers.subject,
296
+ content: message.content,
297
+ },
298
+ });
299
+ })();
300
+ });
301
+ const timer = setTimeout(() => {
302
+ if (settled)
303
+ return;
304
+ settled = true;
305
+ unsubscribe();
306
+ resolve(errorResult(call.id, `Timed out after ${timeoutSeconds}s waiting for a matching message`, "timeout"));
307
+ }, timeoutSeconds * 1000);
308
+ // Respect the abort signal.
309
+ signal.addEventListener("abort", () => {
310
+ if (settled)
311
+ return;
312
+ settled = true;
313
+ unsubscribe();
314
+ clearTimeout(timer);
315
+ resolve(errorResult(call.id, "aborted", "aborted"));
316
+ }, { once: true });
317
+ });
318
+ };
319
+ }
320
+ // ---------------------------------------------------------------------------
321
+ // Helper
322
+ // ---------------------------------------------------------------------------
323
+ function errorResult(callId, message, code) {
324
+ const content = { error: message };
325
+ if (code !== undefined) {
326
+ content["code"] = code;
327
+ }
328
+ return { callId, content, isError: true };
329
+ }
@@ -0,0 +1,12 @@
1
+ import type { ToolDefinition, ToolRunner } from "@intx/types/runtime";
2
+ import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities";
3
+ export { TOOL_DEFINITIONS } from "./definitions.js";
4
+ export type { MailToolName } from "./definitions.js";
5
+ export interface MailToolsOptions {
6
+ capabilities: RuntimeCapabilities;
7
+ }
8
+ export interface MailTools extends ToolRunner {
9
+ readonly definitions: ToolDefinition[];
10
+ dispose(): Promise<void>;
11
+ }
12
+ export declare function createMailTools(opts: MailToolsOptions): MailTools;
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ // Public surface for @intx/tools-mail.
2
+ //
3
+ // createMailTools resolves the bound agent's MessageTransport from the
4
+ // supplied RuntimeCapabilities once at handler-init and wires the five
5
+ // mail handlers around it. The returned MailTools satisfies the
6
+ // ToolRunner contract the harness consumes.
7
+ import { TOOL_DEFINITIONS } from "./definitions.js";
8
+ import { makeMailReadHandler, makeMailReplyHandler, makeMailSearchHandler, makeMailSendHandler, makeMailWaitHandler, } from "./handlers.js";
9
+ export { TOOL_DEFINITIONS } from "./definitions.js";
10
+ export function createMailTools(opts) {
11
+ // Resolve the transport once at handler-init. The lifecycle contract is
12
+ // "request once at handler-init, hold the handle for the deploy
13
+ // lifetime"; the handler factories below close over the resolved
14
+ // handle and do not re-consult capabilities.
15
+ const transport = opts.capabilities.resolve("mail.transport");
16
+ const handlers = new Map([
17
+ ["mail_send", makeMailSendHandler(transport)],
18
+ ["mail_reply", makeMailReplyHandler(transport)],
19
+ ["mail_search", makeMailSearchHandler(transport)],
20
+ ["mail_read", makeMailReadHandler(transport)],
21
+ ["mail_wait", makeMailWaitHandler(transport)],
22
+ ]);
23
+ let disposed = false;
24
+ return {
25
+ definitions: TOOL_DEFINITIONS,
26
+ async run(call, signal) {
27
+ const handler = handlers.get(call.name);
28
+ if (handler === undefined) {
29
+ // This branch is unreachable in the sidecar composition: the
30
+ // agent's `resolveTools` dispatches `call.name` to the owning
31
+ // bundle by definition.name, so only names this runner
32
+ // declared can ever reach `run`. It exists so callers that
33
+ // use createMailTools as a standalone ToolRunner (rather than
34
+ // through `defineMailTools`) get the package's native
35
+ // object-shaped error.
36
+ return {
37
+ callId: call.id,
38
+ content: { error: `Unknown tool: "${call.name}"` },
39
+ isError: true,
40
+ };
41
+ }
42
+ try {
43
+ return await handler(call, signal);
44
+ }
45
+ catch (err) {
46
+ // Match the per-handler errorResult shape so consumers see a
47
+ // single error-content shape regardless of which path produced
48
+ // it: { error: <string>, code?: <string> }.
49
+ const message = err instanceof Error ? err.message : `unknown error: ${String(err)}`;
50
+ return {
51
+ callId: call.id,
52
+ content: { error: message },
53
+ isError: true,
54
+ };
55
+ }
56
+ },
57
+ async dispose() {
58
+ if (disposed)
59
+ return;
60
+ disposed = true;
61
+ // No-op today: the transport is owned by the host that
62
+ // constructed it, and the only handler with active resources
63
+ // (mail_wait subscribes via transport.watch and registers a
64
+ // setTimeout / abort listener) releases them through the
65
+ // per-call AbortSignal rather than through this dispose hook.
66
+ // dispose exists for symmetry with createPosixTools and as a
67
+ // seam for any future per-package resources; callers must not
68
+ // rely on it to cancel in-flight tool calls.
69
+ },
70
+ };
71
+ }
@@ -0,0 +1,17 @@
1
+ import { type BaseEnv } from "@intx/agent";
2
+ import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities";
3
+ /**
4
+ * Env contract for the mail tool bundle. Extends `BaseEnv` with the
5
+ * host-assembled `capabilities` -- from which the mail tools resolve
6
+ * `mail.transport` -- and the agent `address`.
7
+ */
8
+ export interface MailToolEnv extends BaseEnv {
9
+ capabilities: RuntimeCapabilities;
10
+ address: string;
11
+ }
12
+ /**
13
+ * Named export the loader picks up. The id is package-namespaced per
14
+ * the convention; the model-facing tool names are synthesized by the
15
+ * loader as `@intx/tools-mail/sidecar-bundle:<def.name>`.
16
+ */
17
+ export declare const mail: import("@intx/agent").AnnotatedToolFactory<MailToolEnv>;
@@ -0,0 +1,30 @@
1
+ // Sidecar-bundle entry for `@intx/tools-mail` — the convention-compliant
2
+ // factory the tool-package loader invokes.
3
+ //
4
+ // The bundle consumes the host-assembled runtime capabilities rather than
5
+ // building its own. The host (the sidecar's step-env builder) owns the
6
+ // `RuntimeCapabilities` and puts it on `env.capabilities`; this factory
7
+ // resolves `mail.transport` from it through `createMailTools` instead of
8
+ // re-wrapping a raw transport it was handed separately. The env keys it
9
+ // touches (`capabilities`, `address`) are declared in `requires`.
10
+ import { defineTool } from "@intx/agent";
11
+ import { createMailTools } from "./index.js";
12
+ import { TOOL_DEFINITIONS } from "./definitions.js";
13
+ /**
14
+ * Named export the loader picks up. The id is package-namespaced per
15
+ * the convention; the model-facing tool names are synthesized by the
16
+ * loader as `@intx/tools-mail/sidecar-bundle:<def.name>`.
17
+ */
18
+ export const mail = defineTool({
19
+ id: "@intx/tools-mail/sidecar-bundle",
20
+ requires: ["capabilities", "address"],
21
+ definitions: TOOL_DEFINITIONS.map((def) => ({ name: def.name })),
22
+ factory: (env) => {
23
+ const tools = createMailTools({ capabilities: env.capabilities });
24
+ return {
25
+ definitions: tools.definitions,
26
+ run: (call, signal) => tools.run(call, signal),
27
+ dispose: () => tools.dispose(),
28
+ };
29
+ },
30
+ });
package/package.json CHANGED
@@ -1,16 +1,39 @@
1
1
  {
2
2
  "name": "@intx/tools-mail",
3
- "version": "0.1.2",
3
+ "description": "Mail tool runner giving agents mail_send, mail_reply, mail_search, mail_read, and mail_wait",
4
+ "version": "0.3.0",
4
5
  "license": "LGPL-2.1-only",
5
6
  "type": "module",
7
+ "interchange": {
8
+ "tools": "./dist/sidecar-bundle.js"
9
+ },
6
10
  "exports": {
7
11
  ".": {
8
- "types": "./src/index.ts",
9
- "default": "./src/index.ts"
12
+ "intx-src": "./src/index.ts",
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./sidecar-bundle": {
17
+ "intx-src": "./src/sidecar-bundle.ts",
18
+ "types": "./dist/sidecar-bundle.d.ts",
19
+ "default": "./dist/sidecar-bundle.js"
10
20
  }
11
21
  },
12
22
  "dependencies": {
13
- "@intx/types": "0.0.0",
23
+ "@intx/agent": "0.3.0",
24
+ "@intx/types": "0.3.0",
14
25
  "arktype": "^2.1.29"
26
+ },
27
+ "devDependencies": {
28
+ "@intx/storage-isogit": "0.3.0"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "sideEffects": false,
36
+ "publishConfig": {
37
+ "access": "public"
15
38
  }
16
39
  }
@@ -1,153 +0,0 @@
1
- // Static definitions for the five mail tools. The catalog generator and the
2
- // inference director both consume these as inert data — no factory calls,
3
- // no runtime side effects.
4
- //
5
- // (MESSAGE.md § Mail Tools)
6
-
7
- import type { ToolDefinition } from "@intx/types/runtime";
8
-
9
- export type MailToolName =
10
- | "mail_send"
11
- | "mail_reply"
12
- | "mail_search"
13
- | "mail_read"
14
- | "mail_wait";
15
-
16
- export const TOOL_DEFINITIONS: ToolDefinition[] = [
17
- {
18
- name: "mail_send",
19
- description:
20
- "Send mail to another agent or address. Use this to initiate conversations or send mail to other agents.",
21
- inputSchema: {
22
- type: "object",
23
- properties: {
24
- to: {
25
- type: "string",
26
- description: "Recipient address (e.g. agent@local.interchange)",
27
- },
28
- content: {
29
- type: "string",
30
- description: "Mail text content",
31
- },
32
- type: {
33
- type: "string",
34
- description: "Mail type (default: conversation.message)",
35
- default: "conversation.message",
36
- },
37
- subject: {
38
- type: "string",
39
- description: "Optional subject line",
40
- },
41
- inReplyTo: {
42
- type: "string",
43
- description: "Message-ID of the mail being replied to",
44
- },
45
- },
46
- required: ["to", "content"],
47
- },
48
- },
49
- {
50
- name: "mail_reply",
51
- description:
52
- "Reply to a mail by reference. Addresses the reply to the original sender and sets inReplyTo for threading.",
53
- inputSchema: {
54
- type: "object",
55
- properties: {
56
- ref: {
57
- type: "object",
58
- description: "Mail reference { uid, mailbox }",
59
- properties: {
60
- uid: { type: "number" },
61
- mailbox: { type: "string" },
62
- },
63
- required: ["uid", "mailbox"],
64
- },
65
- content: {
66
- type: "string",
67
- description: "Reply mail text content",
68
- },
69
- type: {
70
- type: "string",
71
- description: "Mail type (default: conversation.message)",
72
- default: "conversation.message",
73
- },
74
- },
75
- required: ["ref", "content"],
76
- },
77
- },
78
- {
79
- name: "mail_search",
80
- description: "Search mail in a mailbox. Returns mail summaries.",
81
- inputSchema: {
82
- type: "object",
83
- properties: {
84
- mailbox: {
85
- type: "string",
86
- description: "Mailbox to search",
87
- default: "INBOX",
88
- },
89
- query: {
90
- type: "object",
91
- description: "Search query (e.g. { from: 'agent@...' })",
92
- },
93
- limit: {
94
- type: "number",
95
- description: "Maximum results to return",
96
- default: 20,
97
- },
98
- },
99
- },
100
- },
101
- {
102
- name: "mail_read",
103
- description: "Read a specific mail by reference.",
104
- inputSchema: {
105
- type: "object",
106
- properties: {
107
- ref: {
108
- type: "object",
109
- description: "Mail reference { uid, mailbox }",
110
- properties: {
111
- uid: { type: "number" },
112
- mailbox: { type: "string" },
113
- },
114
- required: ["uid", "mailbox"],
115
- },
116
- parts: {
117
- type: "string",
118
- description:
119
- "What to fetch: 'full', 'headers', 'payload', or a MIME part path",
120
- default: "payload",
121
- },
122
- },
123
- required: ["ref"],
124
- },
125
- },
126
- {
127
- name: "mail_wait",
128
- description:
129
- "Wait for mail matching a query to arrive. Blocks until matching mail is delivered or the timeout expires. Use this instead of polling mail_search in a loop.",
130
- inputSchema: {
131
- type: "object",
132
- properties: {
133
- query: {
134
- type: "object",
135
- description:
136
- "Search criteria for the mail to wait for (e.g. { from: 'agent@...' })",
137
- },
138
- timeout: {
139
- type: "number",
140
- description:
141
- "Maximum seconds to wait before returning a timeout error",
142
- default: 120,
143
- },
144
- mailbox: {
145
- type: "string",
146
- description: "Mailbox to watch",
147
- default: "INBOX",
148
- },
149
- },
150
- required: ["query"],
151
- },
152
- },
153
- ];