auggy 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.
- package/CHANGELOG.md +96 -0
- package/LICENSE +201 -0
- package/README.md +161 -0
- package/package.json +76 -0
- package/src/agent-card.ts +39 -0
- package/src/agent.ts +283 -0
- package/src/agentmail-client.ts +138 -0
- package/src/augments/bash/index.ts +463 -0
- package/src/augments/bash/skill/SKILL.md +156 -0
- package/src/augments/budgets/budget-store.ts +513 -0
- package/src/augments/budgets/index.ts +134 -0
- package/src/augments/budgets/preamble.ts +93 -0
- package/src/augments/budgets/types.ts +89 -0
- package/src/augments/file-memory/index.ts +71 -0
- package/src/augments/filesystem/index.ts +533 -0
- package/src/augments/filesystem/skill/SKILL.md +142 -0
- package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
- package/src/augments/layered-memory/extractor/buffer.ts +56 -0
- package/src/augments/layered-memory/extractor/frequency.ts +79 -0
- package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
- package/src/augments/layered-memory/extractor/parse.ts +75 -0
- package/src/augments/layered-memory/extractor/prompt.md +26 -0
- package/src/augments/layered-memory/index.ts +757 -0
- package/src/augments/layered-memory/skill/SKILL.md +153 -0
- package/src/augments/layered-memory/storage/migrations/README.md +16 -0
- package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
- package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
- package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
- package/src/augments/layered-memory/storage/types.ts +98 -0
- package/src/augments/link/index.ts +489 -0
- package/src/augments/link/translate.ts +261 -0
- package/src/augments/notify/adapters/agentmail.ts +70 -0
- package/src/augments/notify/adapters/telegram.ts +60 -0
- package/src/augments/notify/adapters/webhook.ts +55 -0
- package/src/augments/notify/index.ts +284 -0
- package/src/augments/notify/skill/SKILL.md +150 -0
- package/src/augments/org-context/index.ts +721 -0
- package/src/augments/org-context/skill/SKILL.md +96 -0
- package/src/augments/skills/index.ts +103 -0
- package/src/augments/supabase-memory/index.ts +151 -0
- package/src/augments/telegram-transport/index.ts +312 -0
- package/src/augments/telegram-transport/polling.ts +55 -0
- package/src/augments/telegram-transport/webhook.ts +56 -0
- package/src/augments/turn-control/index.ts +61 -0
- package/src/augments/turn-control/skill/SKILL.md +155 -0
- package/src/augments/visitor-auth/email-validation.ts +66 -0
- package/src/augments/visitor-auth/index.ts +779 -0
- package/src/augments/visitor-auth/rate-limiter.ts +90 -0
- package/src/augments/visitor-auth/skill/SKILL.md +55 -0
- package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
- package/src/augments/visitor-auth/storage/types.ts +164 -0
- package/src/augments/visitor-auth/types.ts +123 -0
- package/src/augments/visitor-auth/verify-page.ts +179 -0
- package/src/augments/web-fetch/index.ts +331 -0
- package/src/augments/web-fetch/skill/SKILL.md +100 -0
- package/src/cli/agent-index.ts +289 -0
- package/src/cli/augment-catalog.ts +320 -0
- package/src/cli/augment-resolver.ts +597 -0
- package/src/cli/commands/add-skill.ts +194 -0
- package/src/cli/commands/add.ts +87 -0
- package/src/cli/commands/chat.ts +207 -0
- package/src/cli/commands/create.ts +462 -0
- package/src/cli/commands/dev.ts +139 -0
- package/src/cli/commands/eval.ts +180 -0
- package/src/cli/commands/ls.ts +66 -0
- package/src/cli/commands/remove.ts +95 -0
- package/src/cli/commands/restart.ts +40 -0
- package/src/cli/commands/start.ts +123 -0
- package/src/cli/commands/status.ts +104 -0
- package/src/cli/commands/stop.ts +84 -0
- package/src/cli/commands/visitors-revoke.ts +155 -0
- package/src/cli/commands/visitors.ts +101 -0
- package/src/cli/config-parser.ts +1034 -0
- package/src/cli/engine-resolver.ts +68 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/model-picker.ts +89 -0
- package/src/cli/pid-registry.ts +146 -0
- package/src/cli/plist-generator.ts +117 -0
- package/src/cli/resolve-config.ts +56 -0
- package/src/cli/scaffold-skills.ts +158 -0
- package/src/cli/scaffold.ts +291 -0
- package/src/cli/skill-frontmatter.ts +51 -0
- package/src/cli/skill-validator.ts +151 -0
- package/src/cli/types.ts +228 -0
- package/src/cli/yaml-helpers.ts +66 -0
- package/src/engines/_shared/cost.ts +55 -0
- package/src/engines/_shared/schema-normalize.ts +75 -0
- package/src/engines/anthropic/pricing.ts +117 -0
- package/src/engines/anthropic.ts +483 -0
- package/src/engines/openai/pricing.ts +67 -0
- package/src/engines/openai.ts +446 -0
- package/src/engines/openrouter/pricing.ts +83 -0
- package/src/engines/openrouter.ts +185 -0
- package/src/helpers.ts +24 -0
- package/src/http.ts +387 -0
- package/src/index.ts +165 -0
- package/src/kernel/capability-table.ts +172 -0
- package/src/kernel/context-allocator.ts +161 -0
- package/src/kernel/history-manager.ts +198 -0
- package/src/kernel/lifecycle-manager.ts +106 -0
- package/src/kernel/output-validator.ts +35 -0
- package/src/kernel/preamble.ts +23 -0
- package/src/kernel/route-collector.ts +97 -0
- package/src/kernel/timeout.ts +21 -0
- package/src/kernel/tool-selector.ts +47 -0
- package/src/kernel/trace-emitter.ts +66 -0
- package/src/kernel/transport-queue.ts +147 -0
- package/src/kernel/turn-loop.ts +1148 -0
- package/src/memory/context-synthesis.ts +83 -0
- package/src/memory/memory-bus.ts +61 -0
- package/src/memory/registry.ts +80 -0
- package/src/memory/tools.ts +320 -0
- package/src/memory/types.ts +8 -0
- package/src/parts.ts +30 -0
- package/src/scaffold-templates/identity.md +31 -0
- package/src/telegram-client.ts +145 -0
- package/src/tokenizer.ts +14 -0
- package/src/transports/ag-ui-events.ts +253 -0
- package/src/transports/visitor-token.ts +82 -0
- package/src/transports/web-transport.ts +948 -0
- package/src/types.ts +1009 -0
package/src/agent.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentCard,
|
|
3
|
+
AgentConfig,
|
|
4
|
+
AgentHandle,
|
|
5
|
+
AgentHealth,
|
|
6
|
+
ModelClient,
|
|
7
|
+
TurnTrigger,
|
|
8
|
+
TurnResult,
|
|
9
|
+
TransportKernel,
|
|
10
|
+
PeerIdentity,
|
|
11
|
+
OutboundMessage,
|
|
12
|
+
SchedulerContext,
|
|
13
|
+
} from "./types";
|
|
14
|
+
import { createTokenizer } from "./tokenizer";
|
|
15
|
+
import { generateAgentCard } from "./agent-card";
|
|
16
|
+
import { wireMemoryBus } from "./memory/memory-bus";
|
|
17
|
+
import { createTurnLoop } from "./kernel/turn-loop";
|
|
18
|
+
import { createLifecycleManager } from "./kernel/lifecycle-manager";
|
|
19
|
+
import { createTransportQueue } from "./kernel/transport-queue";
|
|
20
|
+
import { collectAugmentRoutes } from "./kernel/route-collector";
|
|
21
|
+
import type { CollectedRoute } from "./kernel/route-collector";
|
|
22
|
+
|
|
23
|
+
export function defineAgent(config: AgentConfig, model: ModelClient): AgentHandle {
|
|
24
|
+
const tokenizer = createTokenizer();
|
|
25
|
+
|
|
26
|
+
// Wire the memory bus BEFORE constructing other kernel components.
|
|
27
|
+
// This synthesizes context() for memory providers and adds a synthetic
|
|
28
|
+
// augment carrying the generic memory tools.
|
|
29
|
+
const wiring = wireMemoryBus(config.augments);
|
|
30
|
+
const effectiveAugments = wiring.syntheticToolsAugment
|
|
31
|
+
? [...wiring.augmentsWithSynthesizedContext, wiring.syntheticToolsAugment]
|
|
32
|
+
: wiring.augmentsWithSynthesizedContext;
|
|
33
|
+
|
|
34
|
+
const effectiveConfig: AgentConfig = {
|
|
35
|
+
...config,
|
|
36
|
+
augments: effectiveAugments,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Agent card is generated from the effective config (includes memory
|
|
40
|
+
// capability if any augment has a memory field).
|
|
41
|
+
const agentCard: AgentCard = generateAgentCard(effectiveConfig);
|
|
42
|
+
|
|
43
|
+
const lifecycle = createLifecycleManager({
|
|
44
|
+
name: effectiveConfig.name,
|
|
45
|
+
augments: effectiveAugments,
|
|
46
|
+
model,
|
|
47
|
+
});
|
|
48
|
+
const turnLoop = createTurnLoop({
|
|
49
|
+
augments: effectiveAugments,
|
|
50
|
+
model,
|
|
51
|
+
tokenizer,
|
|
52
|
+
config: effectiveConfig,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const outboundHandlers = new Map<
|
|
56
|
+
string,
|
|
57
|
+
(peer: PeerIdentity, message: OutboundMessage) => Promise<void>
|
|
58
|
+
>();
|
|
59
|
+
|
|
60
|
+
let started = false;
|
|
61
|
+
|
|
62
|
+
async function dispatchOutbound(result: TurnResult, trigger: TurnTrigger) {
|
|
63
|
+
// Collect all messages to dispatch: single response + multi-destination responses
|
|
64
|
+
const messages: OutboundMessage[] = [];
|
|
65
|
+
if (result.response) messages.push(result.response);
|
|
66
|
+
if (result.responses) messages.push(...result.responses);
|
|
67
|
+
if (messages.length === 0) return;
|
|
68
|
+
|
|
69
|
+
for (const msg of messages) {
|
|
70
|
+
const targetAugment = msg.targetAugment ?? trigger.source;
|
|
71
|
+
const peer = trigger.peer;
|
|
72
|
+
if (!targetAugment || !peer) continue;
|
|
73
|
+
const handler = outboundHandlers.get(targetAugment);
|
|
74
|
+
if (handler) {
|
|
75
|
+
await handler(peer, msg);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const handle: AgentHandle = {
|
|
81
|
+
async start() {
|
|
82
|
+
if (started) throw new Error("Agent already started. Call stop() first.");
|
|
83
|
+
await lifecycle.boot();
|
|
84
|
+
|
|
85
|
+
// PR γ.1 — collect augment-registered HTTP routes AFTER boot so
|
|
86
|
+
// onBoot-populated route lists are visible, BEFORE any transport
|
|
87
|
+
// binds a port so a collision can't leave the agent half-bound.
|
|
88
|
+
const collected = collectAugmentRoutes(effectiveAugments);
|
|
89
|
+
if (collected.errors.length > 0) {
|
|
90
|
+
// Run shutdown to undo the boot side-effects we just performed
|
|
91
|
+
// (otherwise SQLite handles, file watchers, etc. leak).
|
|
92
|
+
try {
|
|
93
|
+
await lifecycle.shutdown();
|
|
94
|
+
} catch {
|
|
95
|
+
// best-effort; the original validation error wins
|
|
96
|
+
}
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Cannot start agent — augment HTTP route validation failed:\n ` +
|
|
99
|
+
collected.errors.join("\n "),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const augmentRoutes: readonly CollectedRoute[] = collected.routes;
|
|
103
|
+
|
|
104
|
+
// Register transport augments
|
|
105
|
+
for (const aug of effectiveAugments) {
|
|
106
|
+
if (aug.transport) {
|
|
107
|
+
const queue = createTransportQueue({
|
|
108
|
+
concurrency: aug.transport.concurrency ?? 1,
|
|
109
|
+
maxQueueDepth: aug.transport.maxQueueDepth ?? 50,
|
|
110
|
+
rateLimitPerPeer: aug.transport.rateLimitPerPeer,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const transportKernel: TransportKernel = {
|
|
114
|
+
async handleInbound(
|
|
115
|
+
trigger: TurnTrigger,
|
|
116
|
+
opts?: { onEvent?: import("./types").KernelEventHandler },
|
|
117
|
+
): Promise<TurnResult> {
|
|
118
|
+
return queue.enqueue(trigger, async (t) => {
|
|
119
|
+
lifecycle.resetIdleTimer();
|
|
120
|
+
const threadId = t.threadId ?? t.turnId;
|
|
121
|
+
const result = await turnLoop.executeTurn(t, threadId, {
|
|
122
|
+
onEvent: opts?.onEvent,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await dispatchOutbound(result, t);
|
|
126
|
+
|
|
127
|
+
// Eager compaction
|
|
128
|
+
const historyBudget = Math.floor(
|
|
129
|
+
model.maxContextTokens * ((config.contextBudget?.historyPercent ?? 40) / 100),
|
|
130
|
+
);
|
|
131
|
+
turnLoop
|
|
132
|
+
.getHistoryManager(threadId)
|
|
133
|
+
.compact(historyBudget, config.compactionStrategy ?? "truncate");
|
|
134
|
+
|
|
135
|
+
// Run onTurnEnd hooks. Awaited sequentially in declaration
|
|
136
|
+
// order so ADR-027's lifecycle ordering guarantee holds —
|
|
137
|
+
// scheduleAfterTurn must observe a fully-settled onTurnEnd.
|
|
138
|
+
for (const a of effectiveAugments) {
|
|
139
|
+
if (a.onTurnEnd) {
|
|
140
|
+
try {
|
|
141
|
+
await a.onTurnEnd(result);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.warn(`onTurnEnd hook "${a.name}" failed: ${err}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ADR-027: dispatch scheduleAfterTurn (same semantics as the
|
|
149
|
+
// inject() path) — sequential, error-isolated, transcript
|
|
150
|
+
// closure-bound to the just-completed turn. The hook runs
|
|
151
|
+
// here as well as in inject() so transport-driven turns
|
|
152
|
+
// (web/Telegram) get the same post-turn surface as kernel-
|
|
153
|
+
// injected ones; PR β's auto-save depends on both paths
|
|
154
|
+
// firing identically.
|
|
155
|
+
const completedTurnIdT = result.turnId;
|
|
156
|
+
const completedThreadIdT = threadId;
|
|
157
|
+
const ctxT: SchedulerContext = {
|
|
158
|
+
inject: (next) => handle.inject(next),
|
|
159
|
+
getCompletedTranscript: async () =>
|
|
160
|
+
turnLoop.getHistoryManager(completedThreadIdT).getTranscript(completedTurnIdT),
|
|
161
|
+
};
|
|
162
|
+
for (const a of effectiveAugments) {
|
|
163
|
+
if (a.scheduleAfterTurn) {
|
|
164
|
+
try {
|
|
165
|
+
await a.scheduleAfterTurn(result, ctxT);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.warn(
|
|
168
|
+
`scheduleAfterTurn hook "${a.name}" threw: ${(err as Error).message}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result;
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
onOutbound(callback) {
|
|
178
|
+
outboundHandlers.set(aug.name, callback);
|
|
179
|
+
},
|
|
180
|
+
getAgentCard() {
|
|
181
|
+
return agentCard;
|
|
182
|
+
},
|
|
183
|
+
getAugmentRoutes() {
|
|
184
|
+
return augmentRoutes;
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
await aug.transport.register(transportKernel, aug.name);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Start idle timer
|
|
192
|
+
lifecycle.startIdleTimer(async () => {
|
|
193
|
+
for (const aug of effectiveAugments) {
|
|
194
|
+
if (aug.onIdle) {
|
|
195
|
+
try {
|
|
196
|
+
await aug.onIdle();
|
|
197
|
+
} catch {
|
|
198
|
+
// Log and continue
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
started = true;
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
async stop() {
|
|
208
|
+
if (!started) return; // no-op if not started
|
|
209
|
+
lifecycle.stopIdleTimer();
|
|
210
|
+
await lifecycle.shutdown();
|
|
211
|
+
started = false;
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
async ready() {
|
|
215
|
+
if (!started) throw new Error("Agent not started");
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
health(): AgentHealth {
|
|
219
|
+
return lifecycle.health();
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
card(): AgentCard {
|
|
223
|
+
return agentCard;
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
async inject(trigger: TurnTrigger): Promise<TurnResult> {
|
|
227
|
+
lifecycle.resetIdleTimer();
|
|
228
|
+
const threadId = trigger.threadId ?? trigger.turnId;
|
|
229
|
+
const result = await turnLoop.executeTurn(trigger, threadId);
|
|
230
|
+
|
|
231
|
+
await dispatchOutbound(result, trigger);
|
|
232
|
+
|
|
233
|
+
// Eager compaction
|
|
234
|
+
const historyBudget = Math.floor(
|
|
235
|
+
model.maxContextTokens * ((config.contextBudget?.historyPercent ?? 40) / 100),
|
|
236
|
+
);
|
|
237
|
+
turnLoop
|
|
238
|
+
.getHistoryManager(threadId)
|
|
239
|
+
.compact(historyBudget, config.compactionStrategy ?? "truncate");
|
|
240
|
+
|
|
241
|
+
// Run onTurnEnd hooks. Awaited sequentially in declaration order so
|
|
242
|
+
// that ADR-027's lifecycle ordering guarantee holds — scheduleAfterTurn
|
|
243
|
+
// must observe a fully-settled onTurnEnd. Errors are caught/logged so
|
|
244
|
+
// a single failing hook never propagates out of the inject path.
|
|
245
|
+
for (const a of effectiveAugments) {
|
|
246
|
+
if (a.onTurnEnd) {
|
|
247
|
+
try {
|
|
248
|
+
await a.onTurnEnd(result);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.warn(`onTurnEnd hook "${a.name}" failed: ${err}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ADR-027: dispatch scheduleAfterTurn for augments that registered it.
|
|
256
|
+
// SchedulerContext closes over inject (this handle's method) +
|
|
257
|
+
// getCompletedTranscript (closure-bound to the just-completed turn id;
|
|
258
|
+
// arbitrary-turnId reads stay kernel-internal at v1.0). Sequential
|
|
259
|
+
// execution in declaration order per ADR-027 Decision 2; errors are
|
|
260
|
+
// caught + logged, never propagated — background work is best-effort.
|
|
261
|
+
const completedTurnId = result.turnId;
|
|
262
|
+
const completedThreadId = threadId;
|
|
263
|
+
const ctx: SchedulerContext = {
|
|
264
|
+
inject: (t) => handle.inject(t),
|
|
265
|
+
getCompletedTranscript: async () =>
|
|
266
|
+
turnLoop.getHistoryManager(completedThreadId).getTranscript(completedTurnId),
|
|
267
|
+
};
|
|
268
|
+
for (const a of effectiveAugments) {
|
|
269
|
+
if (a.scheduleAfterTurn) {
|
|
270
|
+
try {
|
|
271
|
+
await a.scheduleAfterTurn(result, ctx);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.warn(`scheduleAfterTurn hook "${a.name}" threw: ${(err as Error).message}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return result;
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
return handle;
|
|
283
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentMail HTTP client — stateless infrastructure shared by the notify
|
|
3
|
+
* agentmail adapter and (future) the agentMail augment.
|
|
4
|
+
*
|
|
5
|
+
* Pattern matches src/telegram-client.ts: env-var or constructor-arg keyed,
|
|
6
|
+
* no SQLite state, no augment-system coupling.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHttpClient } from "./http";
|
|
10
|
+
import type { HttpClient } from "./http";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_BASE_URL = "https://api.agentmail.to/v0";
|
|
13
|
+
|
|
14
|
+
export interface AgentMailClientOptions {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
/** Override AgentMail API base URL (testing/sandbox). */
|
|
17
|
+
apiBaseUrl?: string;
|
|
18
|
+
/** Timeout per request. Default 15s. */
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
/** Test-only HTTP client override. */
|
|
21
|
+
http?: Pick<HttpClient, "post" | "get">;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AgentMailInboxInfo {
|
|
25
|
+
inboxId: string;
|
|
26
|
+
/** Echoed back when the inbox exists. */
|
|
27
|
+
status: "ok";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AgentMailInboxError {
|
|
31
|
+
status: "failed";
|
|
32
|
+
detail: string;
|
|
33
|
+
/** HTTP status if the failure originated from AgentMail (vs. network). */
|
|
34
|
+
httpStatus?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SendMessageInput {
|
|
38
|
+
inboxId: string;
|
|
39
|
+
to: string[];
|
|
40
|
+
subject: string;
|
|
41
|
+
text: string;
|
|
42
|
+
html?: string;
|
|
43
|
+
labels?: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SendMessageResult {
|
|
47
|
+
status: "sent";
|
|
48
|
+
messageId: string;
|
|
49
|
+
threadId: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SendMessageError {
|
|
53
|
+
status: "failed";
|
|
54
|
+
detail: string;
|
|
55
|
+
/** HTTP status if the failure originated from AgentMail (vs. network). */
|
|
56
|
+
httpStatus?: number;
|
|
57
|
+
/** AgentMail-returned Retry-After if 429. */
|
|
58
|
+
retryAfterSec?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface AgentMailClient {
|
|
62
|
+
send(input: SendMessageInput): Promise<SendMessageResult | SendMessageError>;
|
|
63
|
+
/**
|
|
64
|
+
* Best-effort healthcheck. Pings AgentMail's `inboxes.get` endpoint to
|
|
65
|
+
* confirm the inbox exists and the API key has access. Used by visitorAuth
|
|
66
|
+
* onBoot. Caller should warn-and-continue on failure: a transient AgentMail
|
|
67
|
+
* outage shouldn't block agent startup; the first real send will surface
|
|
68
|
+
* the same error.
|
|
69
|
+
*/
|
|
70
|
+
getInbox(inboxId: string): Promise<AgentMailInboxInfo | AgentMailInboxError>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createAgentMailClient(opts: AgentMailClientOptions): AgentMailClient {
|
|
74
|
+
const baseUrl = opts.apiBaseUrl ?? DEFAULT_BASE_URL;
|
|
75
|
+
const http =
|
|
76
|
+
opts.http ??
|
|
77
|
+
createHttpClient({
|
|
78
|
+
timeoutMs: opts.timeoutMs ?? 15_000,
|
|
79
|
+
userAgent: "auggy-agentmail-client/0.1",
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
async send(input) {
|
|
83
|
+
const url = `${baseUrl}/inboxes/${input.inboxId}/messages`;
|
|
84
|
+
const body = JSON.stringify({
|
|
85
|
+
to: input.to,
|
|
86
|
+
subject: input.subject,
|
|
87
|
+
text: input.text,
|
|
88
|
+
...(input.html ? { html: input.html } : {}),
|
|
89
|
+
...(input.labels && input.labels.length > 0 ? { labels: input.labels } : {}),
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
const res = await http.post(url, {
|
|
93
|
+
headers: {
|
|
94
|
+
"content-type": "application/json",
|
|
95
|
+
authorization: `Bearer ${opts.apiKey}`,
|
|
96
|
+
},
|
|
97
|
+
body,
|
|
98
|
+
});
|
|
99
|
+
if (res.status < 200 || res.status >= 300) {
|
|
100
|
+
const result: SendMessageError = {
|
|
101
|
+
status: "failed",
|
|
102
|
+
detail: `agentmail returned ${res.status}: ${res.body.slice(0, 200)}`,
|
|
103
|
+
httpStatus: res.status,
|
|
104
|
+
};
|
|
105
|
+
if (res.status === 429) {
|
|
106
|
+
const retry = res.headers.get("retry-after");
|
|
107
|
+
if (retry) result.retryAfterSec = Number(retry) || undefined;
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
const parsed = JSON.parse(res.body) as { message_id: string; thread_id: string };
|
|
112
|
+
return { status: "sent", messageId: parsed.message_id, threadId: parsed.thread_id };
|
|
113
|
+
} catch (err) {
|
|
114
|
+
return { status: "failed", detail: `agentmail error: ${(err as Error).message}` };
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
async getInbox(inboxId: string) {
|
|
118
|
+
const url = `${baseUrl}/inboxes/${inboxId}`;
|
|
119
|
+
try {
|
|
120
|
+
const res = await http.get(url, {
|
|
121
|
+
headers: {
|
|
122
|
+
authorization: `Bearer ${opts.apiKey}`,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
if (res.status < 200 || res.status >= 300) {
|
|
126
|
+
return {
|
|
127
|
+
status: "failed" as const,
|
|
128
|
+
detail: `agentmail returned ${res.status}: ${res.body.slice(0, 200)}`,
|
|
129
|
+
httpStatus: res.status,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return { inboxId, status: "ok" as const };
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return { status: "failed" as const, detail: `agentmail error: ${(err as Error).message}` };
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|