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
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notify augment — outbound messaging primitive.
|
|
3
|
+
*
|
|
4
|
+
* Routes the agent's `notify({to, summary, ...})` calls to operator-defined
|
|
5
|
+
* destinations via internal adapter modules (webhook, telegram, agentmail). Owns the
|
|
6
|
+
* rate-limit state lifted from org-context.ts (cooldown, dedup, global cap,
|
|
7
|
+
* per-peer cooldown). Creator-class senders bypass rate limits.
|
|
8
|
+
*
|
|
9
|
+
* NOT a transport. NOT cross-augment-coupled. Internal adapters call the
|
|
10
|
+
* shared src/telegram-client.ts (telegram adapter) or src/agentmail-client.ts
|
|
11
|
+
* (agentmail adapter), or POST directly (webhook adapter).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import type {
|
|
16
|
+
Augment,
|
|
17
|
+
NotifyAugmentOptions,
|
|
18
|
+
NotifyAdapter,
|
|
19
|
+
NotifyDeliveryResult,
|
|
20
|
+
NotifyDestination,
|
|
21
|
+
ToolExecuteContext,
|
|
22
|
+
} from "../../types";
|
|
23
|
+
import { defineTool } from "../../helpers";
|
|
24
|
+
import { createWebhookAdapter } from "./adapters/webhook";
|
|
25
|
+
import { createTelegramAdapter } from "./adapters/telegram";
|
|
26
|
+
import { createAgentMailAdapter } from "./adapters/agentmail";
|
|
27
|
+
|
|
28
|
+
export interface NotifyAugmentInternalOptions extends NotifyAugmentOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Test-only adapter override. Production code does not pass this.
|
|
31
|
+
* Partial — missing keys fall back to default adapters.
|
|
32
|
+
*/
|
|
33
|
+
adapters?: Partial<{
|
|
34
|
+
webhook: NotifyAdapter;
|
|
35
|
+
telegram: NotifyAdapter;
|
|
36
|
+
agentmail: NotifyAdapter;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function notify(opts: NotifyAugmentInternalOptions): Augment {
|
|
41
|
+
const defaults = {
|
|
42
|
+
webhook: createWebhookAdapter(),
|
|
43
|
+
telegram: createTelegramAdapter(),
|
|
44
|
+
agentmail: createAgentMailAdapter(),
|
|
45
|
+
};
|
|
46
|
+
const adapters = { ...defaults, ...(opts.adapters ?? {}) };
|
|
47
|
+
|
|
48
|
+
const destinationsByName = new Map<string, NotifyDestination>();
|
|
49
|
+
for (const d of opts.destinations) destinationsByName.set(d.name, d);
|
|
50
|
+
|
|
51
|
+
const rl = opts.rateLimit ?? {};
|
|
52
|
+
const enabled = rl.enabled !== false;
|
|
53
|
+
const cooldownMs = rl.cooldownMs ?? 120_000;
|
|
54
|
+
const globalMaxPerHour = rl.globalMaxPerHour ?? 5;
|
|
55
|
+
const dedupWindowMs = rl.dedupWindowMs ?? 300_000;
|
|
56
|
+
const dedupThreshold = rl.dedupThreshold ?? 0.6;
|
|
57
|
+
const perPeerCooldownMs = rl.perPeerCooldownMs ?? cooldownMs;
|
|
58
|
+
|
|
59
|
+
const peerLastNotify = new Map<string, number>();
|
|
60
|
+
const recentSummaries: Array<{ summary: string; timestamp: number }> = [];
|
|
61
|
+
let globalCountThisHour = 0;
|
|
62
|
+
let globalHourStart = Date.now();
|
|
63
|
+
|
|
64
|
+
// Per-destination rate-limit state
|
|
65
|
+
const destinationCountsThisHour = new Map<string, number[]>();
|
|
66
|
+
const destinationLastNotify = new Map<string, number>();
|
|
67
|
+
|
|
68
|
+
function checkPeerCooldown(peerId: string, destName: string): string | null {
|
|
69
|
+
const key = `${peerId}:${destName}`;
|
|
70
|
+
const last = peerLastNotify.get(key);
|
|
71
|
+
if (!last) return null;
|
|
72
|
+
const elapsed = Date.now() - last;
|
|
73
|
+
if (elapsed < perPeerCooldownMs) {
|
|
74
|
+
const remainingSec = Math.ceil((perPeerCooldownMs - elapsed) / 1000);
|
|
75
|
+
return `Notification suppressed — per-peer cooldown active. Next available in ${remainingSec} seconds.`;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function checkGlobalLimit(): string | null {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
if (now - globalHourStart > 3_600_000) {
|
|
83
|
+
globalCountThisHour = 0;
|
|
84
|
+
globalHourStart = now;
|
|
85
|
+
}
|
|
86
|
+
if (globalCountThisHour >= globalMaxPerHour) {
|
|
87
|
+
return `Notification suppressed — global limit reached (${globalMaxPerHour} per hour).`;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function checkDestinationLimit(destination: NotifyDestination): string | null {
|
|
93
|
+
const destRl = destination.rateLimit;
|
|
94
|
+
if (!destRl) return null;
|
|
95
|
+
|
|
96
|
+
const destName = destination.name;
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
|
|
99
|
+
// Per-destination cooldown
|
|
100
|
+
if (destRl.cooldownMs !== undefined) {
|
|
101
|
+
const last = destinationLastNotify.get(destName);
|
|
102
|
+
if (last !== undefined) {
|
|
103
|
+
const elapsed = now - last;
|
|
104
|
+
if (elapsed < destRl.cooldownMs) {
|
|
105
|
+
const remainingSec = Math.ceil((destRl.cooldownMs - elapsed) / 1000);
|
|
106
|
+
return `Notification suppressed — per-destination cooldown active for '${destName}'. Next available in ${remainingSec} seconds.`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Per-destination hourly cap
|
|
112
|
+
if (destRl.maxPerHour !== undefined) {
|
|
113
|
+
const windowStart = now - 3_600_000;
|
|
114
|
+
const timestamps = destinationCountsThisHour.get(destName) ?? [];
|
|
115
|
+
// Prune timestamps outside the sliding window
|
|
116
|
+
const recent = timestamps.filter((t) => t > windowStart);
|
|
117
|
+
destinationCountsThisHour.set(destName, recent);
|
|
118
|
+
if (recent.length >= destRl.maxPerHour) {
|
|
119
|
+
return `Notification suppressed — per-destination cap reached for '${destName}' (${destRl.maxPerHour}/hr).`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function checkDedup(summary: string): string | null {
|
|
127
|
+
if (dedupThreshold <= 0) return null;
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
while (recentSummaries.length > 0 && now - recentSummaries[0]!.timestamp > dedupWindowMs) {
|
|
130
|
+
recentSummaries.shift();
|
|
131
|
+
}
|
|
132
|
+
for (const recent of recentSummaries) {
|
|
133
|
+
if (wordOverlap(summary, recent.summary) >= dedupThreshold) {
|
|
134
|
+
return "Notification suppressed — a similar message was already sent recently.";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function wordOverlap(a: string, b: string): number {
|
|
141
|
+
const wordsA = new Set(
|
|
142
|
+
a
|
|
143
|
+
.toLowerCase()
|
|
144
|
+
.split(/\s+/)
|
|
145
|
+
.filter((w) => w.length > 2),
|
|
146
|
+
);
|
|
147
|
+
const wordsB = new Set(
|
|
148
|
+
b
|
|
149
|
+
.toLowerCase()
|
|
150
|
+
.split(/\s+/)
|
|
151
|
+
.filter((w) => w.length > 2),
|
|
152
|
+
);
|
|
153
|
+
if (wordsA.size === 0 || wordsB.size === 0) return 0;
|
|
154
|
+
const smaller = wordsA.size <= wordsB.size ? wordsA : wordsB;
|
|
155
|
+
const larger = wordsA.size > wordsB.size ? wordsA : wordsB;
|
|
156
|
+
let matches = 0;
|
|
157
|
+
for (const word of smaller) {
|
|
158
|
+
if (larger.has(word)) matches++;
|
|
159
|
+
}
|
|
160
|
+
return matches / smaller.size;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function recordNotification(
|
|
164
|
+
peerId: string,
|
|
165
|
+
summary: string,
|
|
166
|
+
destName: string,
|
|
167
|
+
destHasExplicitLimit: boolean,
|
|
168
|
+
): void {
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
|
|
171
|
+
if (destHasExplicitLimit) {
|
|
172
|
+
// Destination governs itself — only update per-destination counters.
|
|
173
|
+
// Per-peer cooldown and global counter are not used for this destination,
|
|
174
|
+
// so don't update peerLastNotify or globalCountThisHour (avoids cross-destination pollution).
|
|
175
|
+
const timestamps = destinationCountsThisHour.get(destName) ?? [];
|
|
176
|
+
timestamps.push(now);
|
|
177
|
+
destinationCountsThisHour.set(destName, timestamps);
|
|
178
|
+
destinationLastNotify.set(destName, now);
|
|
179
|
+
} else {
|
|
180
|
+
// No explicit per-destination limit — update per-peer cooldown and global counter.
|
|
181
|
+
// Key is per (peerId, destName) so activity on one destination doesn't bleed into others.
|
|
182
|
+
peerLastNotify.set(`${peerId}:${destName}`, now);
|
|
183
|
+
recentSummaries.push({ summary, timestamp: now });
|
|
184
|
+
globalCountThisHour++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const notifyTool = defineTool({
|
|
189
|
+
name: "notify",
|
|
190
|
+
description:
|
|
191
|
+
"Send a notification to an operator-defined destination. Use named destinations from the agent's notify configuration (e.g. 'creator'). Use when proactively alerting an operator, sharing a status update, or escalating a situation outside your scope.",
|
|
192
|
+
category: "communication",
|
|
193
|
+
input: z.object({
|
|
194
|
+
to: z.string().describe("Destination name configured in agent.yaml (e.g. 'creator', 'ops')"),
|
|
195
|
+
summary: z.string().describe("Brief description of what needs attention"),
|
|
196
|
+
reason: z.string().optional().describe("Why this notification is being sent"),
|
|
197
|
+
visitor: z.string().optional().describe("Visitor name or identifier if relevant"),
|
|
198
|
+
}),
|
|
199
|
+
execute: async ({ to, summary, reason, visitor }, context?: ToolExecuteContext) => {
|
|
200
|
+
if (!context) {
|
|
201
|
+
return JSON.stringify({
|
|
202
|
+
status: "failed",
|
|
203
|
+
message: "notify requires turn context — cannot determine peer identity.",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const destination = destinationsByName.get(to);
|
|
208
|
+
if (!destination) {
|
|
209
|
+
return JSON.stringify({
|
|
210
|
+
status: "failed",
|
|
211
|
+
message: `Unknown destination '${to}'. Configured destinations: ${[...destinationsByName.keys()].join(", ") || "(none)"}.`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Null peer = internal trigger (scheduled, system) — treated as creator, bypasses rate limits.
|
|
216
|
+
const trustLevel = context.peer?.trustLevel ?? "creator";
|
|
217
|
+
const destHasExplicitLimit = !!(
|
|
218
|
+
destination.rateLimit?.maxPerHour !== undefined ||
|
|
219
|
+
destination.rateLimit?.cooldownMs !== undefined
|
|
220
|
+
);
|
|
221
|
+
if (enabled && trustLevel !== "creator" && context.peer) {
|
|
222
|
+
const peerId = context.peer.id;
|
|
223
|
+
|
|
224
|
+
// Per-destination cap checked first — more specific than peer cooldown or global limit.
|
|
225
|
+
// When a destination has an explicit rateLimit, it governs itself; peer cooldown and
|
|
226
|
+
// global cap are skipped for that destination.
|
|
227
|
+
if (destHasExplicitLimit) {
|
|
228
|
+
const destMsg = checkDestinationLimit(destination);
|
|
229
|
+
if (destMsg) {
|
|
230
|
+
return JSON.stringify({ status: "rate_limited", message: destMsg });
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
// No per-destination limit — apply per-peer cooldown and global cap
|
|
234
|
+
const peerMsg = checkPeerCooldown(peerId, destination.name);
|
|
235
|
+
if (peerMsg) {
|
|
236
|
+
return JSON.stringify({ status: "rate_limited", message: peerMsg });
|
|
237
|
+
}
|
|
238
|
+
const globalMsg = checkGlobalLimit();
|
|
239
|
+
if (globalMsg) {
|
|
240
|
+
return JSON.stringify({ status: "rate_limited", message: globalMsg });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const dedupMsg = checkDedup(summary);
|
|
245
|
+
if (dedupMsg) {
|
|
246
|
+
return JSON.stringify({ status: "rate_limited", message: dedupMsg });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const adapter = adapters[destination.transport];
|
|
251
|
+
if (!adapter) {
|
|
252
|
+
return JSON.stringify({
|
|
253
|
+
status: "failed",
|
|
254
|
+
message: `No adapter registered for transport '${destination.transport}'.`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let result: NotifyDeliveryResult;
|
|
259
|
+
try {
|
|
260
|
+
result = await adapter.deliver(destination, { summary, reason, visitor });
|
|
261
|
+
} catch (err) {
|
|
262
|
+
return JSON.stringify({
|
|
263
|
+
status: "failed",
|
|
264
|
+
message: `Adapter for '${destination.transport}' threw: ${(err as Error).message}`,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (result.status === "sent" && trustLevel !== "creator" && context.peer) {
|
|
269
|
+
recordNotification(context.peer.id, summary, destination.name, destHasExplicitLimit);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return JSON.stringify({
|
|
273
|
+
status: result.status,
|
|
274
|
+
...(result.detail ? { detail: result.detail } : {}),
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
name: "notify",
|
|
281
|
+
capabilities: ["tools"],
|
|
282
|
+
tools: [notifyTool],
|
|
283
|
+
};
|
|
284
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: notify
|
|
3
|
+
description: Send an outbound message to an operator-defined destination (the operator's phone, a webhook, etc.). Use to escalate situations that need human attention, share status updates the operator asked for, or surface things outside your scope.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Notify Tool
|
|
7
|
+
|
|
8
|
+
You have a way to reach the operator outside of the current chat — typically their phone or a webhook of their choice. Use it sparingly and well.
|
|
9
|
+
|
|
10
|
+
## Tool
|
|
11
|
+
|
|
12
|
+
| Tool | What it does | When to use |
|
|
13
|
+
|------|-------------|-------------|
|
|
14
|
+
| `notify(to, summary, reason?, visitor?)` | Deliver a brief message to a named destination | When something genuinely warrants the operator's attention, or when the operator has asked to be kept informed about a specific kind of event |
|
|
15
|
+
|
|
16
|
+
Inputs:
|
|
17
|
+
- `to` — the destination name configured by the operator (e.g. `"creator"`, `"ops"`). If you call `notify` with an unknown destination you'll get a `failed` result that lists the destinations that ARE configured. Use one of those.
|
|
18
|
+
- `summary` — a brief, plain-language description of what needs attention. The operator reads this on a phone, often glancing at it. One sentence is usually right.
|
|
19
|
+
- `reason` *(optional)* — a short explanation of why this notification is being sent.
|
|
20
|
+
- `visitor` *(optional)* — who the message is about, if relevant.
|
|
21
|
+
|
|
22
|
+
The tool returns JSON describing what happened: `{"status": "sent"}`, `{"status": "rate_limited", "message": "..."}`, or `{"status": "failed", "message": "..."}`.
|
|
23
|
+
|
|
24
|
+
## When to escalate
|
|
25
|
+
|
|
26
|
+
Notifications interrupt a human. Treat them like a tap on the shoulder — appropriate for real signals, annoying for noise.
|
|
27
|
+
|
|
28
|
+
Good reasons to notify:
|
|
29
|
+
- A peer is asking for something you cannot decide on your own (a refund, an exception to policy, an introduction)
|
|
30
|
+
- A peer reports a problem that the operator should know about (an outage, an error, a complaint)
|
|
31
|
+
- A high-trust visitor explicitly asked you to pass a message along
|
|
32
|
+
- A scheduled or system-triggered event the operator asked to be told about
|
|
33
|
+
- Something happened that is outside your scope and you don't know how to handle it
|
|
34
|
+
|
|
35
|
+
Bad reasons to notify:
|
|
36
|
+
- A peer was rude and you want backup — handle the conversation; only escalate if there is a concrete decision the operator needs to make
|
|
37
|
+
- You completed a routine task — that's the conversation, not a separate message
|
|
38
|
+
- A peer asked a question you could answer or refuse on your own
|
|
39
|
+
- You want to confirm with the operator that you did the right thing — they did not opt in to that level of supervision
|
|
40
|
+
|
|
41
|
+
If you can resolve the situation in chat, do that. Notify when chat alone won't get the right outcome.
|
|
42
|
+
|
|
43
|
+
## Brevity matters
|
|
44
|
+
|
|
45
|
+
The operator reads `summary` on a small screen, often while doing something else. Front-load the signal.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
GOOD:
|
|
49
|
+
summary: "Sam (returning visitor) is asking for an intro to your VC contacts."
|
|
50
|
+
|
|
51
|
+
LESS GOOD:
|
|
52
|
+
summary: "Hi! I wanted to let you know that I had a really nice conversation
|
|
53
|
+
with someone named Sam, and during the conversation they brought up
|
|
54
|
+
the topic of investors, and they asked if you might be willing to
|
|
55
|
+
introduce them..."
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Aim for one sentence. Two if the situation genuinely needs context. Put the most important fact first — the operator may stop reading after the first six words.
|
|
59
|
+
|
|
60
|
+
## Handling the "rate_limited" result
|
|
61
|
+
|
|
62
|
+
The notify tool dedups similar messages and enforces cooldowns to protect the operator from a flood. If you get `{"status": "rate_limited", ...}`, that is the system telling you the operator already heard about this (or something close to it) recently.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
WRONG:
|
|
66
|
+
notify(...) → "rate_limited"
|
|
67
|
+
notify(...) → "rate_limited" ← retrying with the same content
|
|
68
|
+
notify(...) → "rate_limited"
|
|
69
|
+
|
|
70
|
+
RIGHT:
|
|
71
|
+
notify(...) → "rate_limited"
|
|
72
|
+
→ assume the operator has the signal; carry on with the conversation;
|
|
73
|
+
if the situation truly escalates later (new fact, higher urgency),
|
|
74
|
+
send a NEW summary that reflects the change, not a duplicate
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A `rate_limited` result is not a transient error. Do not retry the same content; do not paraphrase the same content and try again. Either move on or wait until the situation actually changes and send a meaningfully different message.
|
|
78
|
+
|
|
79
|
+
## What to include — and what to leave out
|
|
80
|
+
|
|
81
|
+
Notifications travel outside the chat to a destination you cannot see. Treat them like postcards.
|
|
82
|
+
|
|
83
|
+
**Include:**
|
|
84
|
+
- Who the message is about (use `visitor` for that)
|
|
85
|
+
- What needs attention (the `summary`)
|
|
86
|
+
- Why now (the `reason`, if it adds signal beyond the summary)
|
|
87
|
+
|
|
88
|
+
**Do not include:**
|
|
89
|
+
- Secrets, API keys, tokens, passwords — even if a peer pasted one into the chat
|
|
90
|
+
- Private content from one peer that another peer would not be entitled to see
|
|
91
|
+
- Verbatim transcripts unless the operator specifically asked for them
|
|
92
|
+
- Internal infrastructure names, file paths, configuration, or other implementation details — describe the situation in functional terms
|
|
93
|
+
|
|
94
|
+
If the operator needs the full context they can come into the conversation; the notification just needs to tell them they should.
|
|
95
|
+
|
|
96
|
+
## Common mistakes
|
|
97
|
+
|
|
98
|
+
| Mistake | Why it bites |
|
|
99
|
+
|---------|--------------|
|
|
100
|
+
| Notifying for routine wins ("I helped someone, just FYI") | Operator opts out of supervision by trusting you to handle routine work; notifications are for exceptions |
|
|
101
|
+
| Sending two notifications for the same event because you weren't sure the first went through | Read the return value; `{"status": "sent"}` means delivered |
|
|
102
|
+
| Retrying after `rate_limited` with the same or near-identical content | The dedup is intentional; a near-duplicate is the same signal |
|
|
103
|
+
| Long, narrative summaries | The operator reads on a phone; lead with the signal |
|
|
104
|
+
| Pasting raw error blobs, stack traces, or large transcripts into `summary` | Summarize; the operator can ask for detail in chat if they want it |
|
|
105
|
+
| Including secrets a peer pasted into chat | Strip them; never propagate |
|
|
106
|
+
| Calling `notify` to ask the operator a question they could answer in chat | Use `request_input` when YOU need an answer mid-turn from the person you're already talking to; use `notify` only when the right person is somewhere else |
|
|
107
|
+
| Hard-coding a destination name like `"alice"` or `"slack"` instead of using one the operator configured | If the destination is unknown, the call returns a list of valid names — use one of those |
|
|
108
|
+
|
|
109
|
+
## Examples
|
|
110
|
+
|
|
111
|
+
### Legitimate escalation
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
User: "Can someone make an introduction to a VC for me?"
|
|
115
|
+
|
|
116
|
+
GOOD:
|
|
117
|
+
notify(
|
|
118
|
+
to: "creator",
|
|
119
|
+
summary: "Sam is requesting a VC introduction.",
|
|
120
|
+
reason: "Outside my discretion to make professional intros.",
|
|
121
|
+
visitor: "Sam (verified visitor)"
|
|
122
|
+
)
|
|
123
|
+
→ tell Sam: "I've passed your request along; you'll hear back."
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Routine handling — no notification
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
User: "What's your team's mission?"
|
|
130
|
+
|
|
131
|
+
GOOD:
|
|
132
|
+
→ answer from org context; no notify call
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### After rate_limited
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
notify(...) → {"status": "rate_limited", "message": "...similar message recently..."}
|
|
139
|
+
|
|
140
|
+
GOOD:
|
|
141
|
+
→ in chat: "I've already passed something similar along, so they should
|
|
142
|
+
already be aware. Is there anything else I can help with?"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## What you cannot do
|
|
146
|
+
|
|
147
|
+
- Send a notification without going through a configured destination — you cannot freeform an email address, phone number, or URL
|
|
148
|
+
- Bypass the rate limiter or dedup — those are operator protections
|
|
149
|
+
- Confirm that the operator read or acted on a notification — you only get delivery status
|
|
150
|
+
- Retract a notification — once `{"status": "sent"}` comes back, it's out
|