@absolutejs/agent-inbox 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/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +367 -0
- package/dist/manifest.d.ts +1 -0
- package/dist/manifest.js +23 -0
- package/dist/manifest.json +19 -0
- package/dist/memory.d.ts +2 -0
- package/dist/postgres.d.ts +15 -0
- package/dist/service.d.ts +69 -0
- package/dist/types.d.ts +117 -0
- package/dist/webhook.d.ts +12 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AbsoluteJS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @absolutejs/agent-inbox
|
|
2
|
+
|
|
3
|
+
Durable inbound work for agents. Verified webhooks and event subscriptions are
|
|
4
|
+
deduplicated by source event, encrypted through an injected codec, leased to one
|
|
5
|
+
worker, retried with backoff, and dead-lettered after a bounded attempt count.
|
|
6
|
+
Interval schedules materialize deterministic occurrences without Redis.
|
|
7
|
+
Occurrences are inserted before a compare-and-swap advances the schedule, so a
|
|
8
|
+
crash cannot lose a tick and retries collapse on the deterministic source ID.
|
|
9
|
+
|
|
10
|
+
Every message pins the target agent's signed discovery identity and preserves
|
|
11
|
+
verification provenance. The runtime adapter starts a durable
|
|
12
|
+
`@absolutejs/agent-runtime` run with the inbox message ID as its idempotency key.
|
|
13
|
+
Webhook bodies are byte-limited and are rejected unless a source-specific
|
|
14
|
+
signature/claim verifier succeeds. The Postgres store uses `SKIP LOCKED`; the
|
|
15
|
+
memory store is intended for tests.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/memory.ts
|
|
3
|
+
var createMemoryAgentInboxStore = () => {
|
|
4
|
+
const subscriptions = new Map;
|
|
5
|
+
const messages = new Map;
|
|
6
|
+
const unique = new Map;
|
|
7
|
+
const schedules = new Map;
|
|
8
|
+
return {
|
|
9
|
+
saveSubscription: async (value) => {
|
|
10
|
+
subscriptions.set(value.id, structuredClone(value));
|
|
11
|
+
},
|
|
12
|
+
listSubscriptions: async ({ tenantId, source, kind }) => [...subscriptions.values()].filter((item) => item.enabled && item.target.tenantId === tenantId && item.source === source && item.kinds.includes(kind)).map((item) => structuredClone(item)),
|
|
13
|
+
enqueue: async (value) => {
|
|
14
|
+
const key = `${value.subscriptionId}:${value.source}:${value.sourceEventId}`;
|
|
15
|
+
const previous = unique.get(key);
|
|
16
|
+
if (previous)
|
|
17
|
+
return structuredClone(messages.get(previous));
|
|
18
|
+
unique.set(key, value.id);
|
|
19
|
+
messages.set(value.id, structuredClone(value));
|
|
20
|
+
return structuredClone(value);
|
|
21
|
+
},
|
|
22
|
+
claim: async ({ workerId, now, leaseExpiresAt }) => {
|
|
23
|
+
const row = [...messages.values()].filter((item) => (item.status === "pending" && Date.parse(item.notBefore) <= Date.parse(now) || item.status === "leased" && Date.parse(item.leaseExpiresAt ?? "") <= Date.parse(now)) && (!item.expiresAt || Date.parse(item.expiresAt) > Date.parse(now))).sort((a, b) => a.notBefore.localeCompare(b.notBefore))[0];
|
|
24
|
+
if (!row)
|
|
25
|
+
return;
|
|
26
|
+
const next = {
|
|
27
|
+
...row,
|
|
28
|
+
status: "leased",
|
|
29
|
+
leaseOwner: workerId,
|
|
30
|
+
leaseExpiresAt,
|
|
31
|
+
attempts: row.attempts + 1,
|
|
32
|
+
updatedAt: now
|
|
33
|
+
};
|
|
34
|
+
messages.set(row.id, next);
|
|
35
|
+
return structuredClone(next);
|
|
36
|
+
},
|
|
37
|
+
complete: async ({ id, workerId, now }) => {
|
|
38
|
+
const row = messages.get(id);
|
|
39
|
+
if (!row || row.status !== "leased" || row.leaseOwner !== workerId)
|
|
40
|
+
return false;
|
|
41
|
+
messages.set(id, {
|
|
42
|
+
...row,
|
|
43
|
+
status: "completed",
|
|
44
|
+
leaseOwner: undefined,
|
|
45
|
+
leaseExpiresAt: undefined,
|
|
46
|
+
updatedAt: now
|
|
47
|
+
});
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
retry: async ({ id, workerId, now, notBefore, error, deadLetter }) => {
|
|
51
|
+
const row = messages.get(id);
|
|
52
|
+
if (!row || row.status !== "leased" || row.leaseOwner !== workerId)
|
|
53
|
+
return false;
|
|
54
|
+
messages.set(id, {
|
|
55
|
+
...row,
|
|
56
|
+
status: deadLetter ? "dead_letter" : "pending",
|
|
57
|
+
leaseOwner: undefined,
|
|
58
|
+
leaseExpiresAt: undefined,
|
|
59
|
+
notBefore,
|
|
60
|
+
lastError: error,
|
|
61
|
+
updatedAt: now
|
|
62
|
+
});
|
|
63
|
+
return true;
|
|
64
|
+
},
|
|
65
|
+
saveSchedule: async (value) => {
|
|
66
|
+
schedules.set(value.id, structuredClone(value));
|
|
67
|
+
},
|
|
68
|
+
claimDueSchedule: async ({ now }) => structuredClone([...schedules.values()].filter((item) => item.enabled && Date.parse(item.nextAt) <= Date.parse(now)).sort((a, b) => a.nextAt.localeCompare(b.nextAt))[0]),
|
|
69
|
+
advanceSchedule: async ({ id, expectedNextAt, nextAt }) => {
|
|
70
|
+
const row = schedules.get(id);
|
|
71
|
+
if (!row || row.nextAt !== expectedNextAt)
|
|
72
|
+
return false;
|
|
73
|
+
schedules.set(id, { ...row, nextAt });
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
// src/postgres.ts
|
|
79
|
+
var safe = (value) => {
|
|
80
|
+
if (!/^[a-z_][a-z0-9_]*$/i.test(value))
|
|
81
|
+
throw new Error("Invalid SQL namespace");
|
|
82
|
+
return value;
|
|
83
|
+
};
|
|
84
|
+
var agentInboxPostgresSchemaSql = (schema = "agent_inbox") => {
|
|
85
|
+
const ns = safe(schema);
|
|
86
|
+
return `CREATE SCHEMA IF NOT EXISTS ${ns}; CREATE TABLE IF NOT EXISTS ${ns}.subscriptions (id text PRIMARY KEY, tenant_id text NOT NULL, source text NOT NULL, kinds text[] NOT NULL, enabled boolean NOT NULL, document jsonb NOT NULL); CREATE INDEX IF NOT EXISTS agent_inbox_subscription_idx ON ${ns}.subscriptions (tenant_id,source) WHERE enabled; CREATE TABLE IF NOT EXISTS ${ns}.messages (id text PRIMARY KEY, subscription_id text NOT NULL, source text NOT NULL, source_event_id text NOT NULL, status text NOT NULL, not_before timestamptz NOT NULL, expires_at timestamptz, lease_owner text, lease_expires_at timestamptz, document jsonb NOT NULL, UNIQUE(subscription_id,source,source_event_id)); CREATE INDEX IF NOT EXISTS agent_inbox_due_idx ON ${ns}.messages (status,not_before,lease_expires_at); CREATE TABLE IF NOT EXISTS ${ns}.schedules (id text PRIMARY KEY, enabled boolean NOT NULL, next_at timestamptz NOT NULL, document jsonb NOT NULL); CREATE INDEX IF NOT EXISTS agent_inbox_schedules_due_idx ON ${ns}.schedules (next_at) WHERE enabled;`;
|
|
87
|
+
};
|
|
88
|
+
var row = (value) => value ? typeof value.document === "string" ? JSON.parse(value.document) : value.document : undefined;
|
|
89
|
+
var createPostgresAgentInboxStore = ({
|
|
90
|
+
client,
|
|
91
|
+
schema = "agent_inbox"
|
|
92
|
+
}) => {
|
|
93
|
+
const ns = safe(schema);
|
|
94
|
+
return {
|
|
95
|
+
saveSubscription: async (v) => {
|
|
96
|
+
await client.query(`INSERT INTO ${ns}.subscriptions (id,tenant_id,source,kinds,enabled,document) VALUES ($1,$2,$3,$4,$5,$6::jsonb) ON CONFLICT (id) DO UPDATE SET tenant_id=$2,source=$3,kinds=$4,enabled=$5,document=$6::jsonb`, [
|
|
97
|
+
v.id,
|
|
98
|
+
v.target.tenantId,
|
|
99
|
+
v.source,
|
|
100
|
+
v.kinds,
|
|
101
|
+
v.enabled,
|
|
102
|
+
JSON.stringify(v)
|
|
103
|
+
]);
|
|
104
|
+
},
|
|
105
|
+
listSubscriptions: async (v) => (await client.query(`SELECT document FROM ${ns}.subscriptions WHERE tenant_id=$1 AND source=$2 AND enabled AND $3=ANY(kinds)`, [v.tenantId, v.source, v.kind])).rows.map((v2) => row(v2)),
|
|
106
|
+
enqueue: async (v) => {
|
|
107
|
+
const result = await client.query(`INSERT INTO ${ns}.messages (id,subscription_id,source,source_event_id,status,not_before,expires_at,document) VALUES ($1,$2,$3,$4,$5,$6::timestamptz,$7::timestamptz,$8::jsonb) ON CONFLICT (subscription_id,source,source_event_id) DO UPDATE SET source_event_id=EXCLUDED.source_event_id RETURNING document`, [
|
|
108
|
+
v.id,
|
|
109
|
+
v.subscriptionId,
|
|
110
|
+
v.source,
|
|
111
|
+
v.sourceEventId,
|
|
112
|
+
v.status,
|
|
113
|
+
v.notBefore,
|
|
114
|
+
v.expiresAt ?? null,
|
|
115
|
+
JSON.stringify(v)
|
|
116
|
+
]);
|
|
117
|
+
return row(result.rows[0]);
|
|
118
|
+
},
|
|
119
|
+
claim: (v) => client.transaction(async (tx) => {
|
|
120
|
+
const found = row((await tx.query(`SELECT document FROM ${ns}.messages WHERE ((status='pending' AND not_before <= $1::timestamptz) OR (status='leased' AND lease_expires_at <= $1::timestamptz)) AND (expires_at IS NULL OR expires_at > $1::timestamptz) ORDER BY not_before FOR UPDATE SKIP LOCKED LIMIT 1`, [v.now])).rows[0]);
|
|
121
|
+
if (!found)
|
|
122
|
+
return;
|
|
123
|
+
const next = {
|
|
124
|
+
...found,
|
|
125
|
+
status: "leased",
|
|
126
|
+
leaseOwner: v.workerId,
|
|
127
|
+
leaseExpiresAt: v.leaseExpiresAt,
|
|
128
|
+
attempts: found.attempts + 1,
|
|
129
|
+
updatedAt: v.now
|
|
130
|
+
};
|
|
131
|
+
return row((await tx.query(`UPDATE ${ns}.messages SET status='leased',lease_owner=$2,lease_expires_at=$3::timestamptz,document=$4::jsonb WHERE id=$1 RETURNING document`, [found.id, v.workerId, v.leaseExpiresAt, JSON.stringify(next)])).rows[0]);
|
|
132
|
+
}),
|
|
133
|
+
complete: async (v) => (await client.query(`UPDATE ${ns}.messages SET status='completed',lease_owner=NULL,lease_expires_at=NULL,document=jsonb_set(jsonb_set(document,'{status}','"completed"'),'{updatedAt}',to_jsonb($3::text)) WHERE id=$1 AND lease_owner=$2 AND status='leased' RETURNING id`, [v.id, v.workerId, v.now])).rows.length === 1,
|
|
134
|
+
retry: async (v) => {
|
|
135
|
+
const status = v.deadLetter ? "dead_letter" : "pending";
|
|
136
|
+
return (await client.query(`UPDATE ${ns}.messages SET status=$3,not_before=$4::timestamptz,lease_owner=NULL,lease_expires_at=NULL,document=document || $5::jsonb WHERE id=$1 AND lease_owner=$2 AND status='leased' RETURNING id`, [
|
|
137
|
+
v.id,
|
|
138
|
+
v.workerId,
|
|
139
|
+
status,
|
|
140
|
+
v.notBefore,
|
|
141
|
+
JSON.stringify({
|
|
142
|
+
status,
|
|
143
|
+
notBefore: v.notBefore,
|
|
144
|
+
lastError: v.error,
|
|
145
|
+
updatedAt: v.now
|
|
146
|
+
})
|
|
147
|
+
])).rows.length === 1;
|
|
148
|
+
},
|
|
149
|
+
saveSchedule: async (v) => {
|
|
150
|
+
await client.query(`INSERT INTO ${ns}.schedules (id,enabled,next_at,document) VALUES ($1,$2,$3::timestamptz,$4::jsonb) ON CONFLICT(id) DO UPDATE SET enabled=$2,next_at=$3::timestamptz,document=$4::jsonb`, [v.id, v.enabled, v.nextAt, JSON.stringify(v)]);
|
|
151
|
+
},
|
|
152
|
+
claimDueSchedule: async (v) => row((await client.query(`SELECT document FROM ${ns}.schedules WHERE enabled AND next_at <= $1::timestamptz ORDER BY next_at LIMIT 1`, [v.now])).rows[0]),
|
|
153
|
+
advanceSchedule: async (v) => (await client.query(`UPDATE ${ns}.schedules SET next_at=$3::timestamptz,document=jsonb_set(document,'{nextAt}',to_jsonb($3::text)) WHERE id=$1 AND next_at=$2::timestamptz RETURNING id`, [v.id, v.expectedNextAt, v.nextAt])).rows.length === 1
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
// src/service.ts
|
|
157
|
+
var identity = {
|
|
158
|
+
encode: async (value) => structuredClone(value),
|
|
159
|
+
decode: async (value) => structuredClone(value)
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
class AgentInboxVerificationError extends Error {
|
|
163
|
+
constructor(message) {
|
|
164
|
+
super(message);
|
|
165
|
+
this.name = "AgentInboxVerificationError";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
var createAgentInbox = ({
|
|
169
|
+
store,
|
|
170
|
+
verifiers,
|
|
171
|
+
codec = identity,
|
|
172
|
+
now = Date.now,
|
|
173
|
+
id = () => crypto.randomUUID(),
|
|
174
|
+
maxBodyBytes = 1048576
|
|
175
|
+
}) => ({
|
|
176
|
+
subscribe: (value) => store.saveSubscription(value),
|
|
177
|
+
schedule: (value) => {
|
|
178
|
+
if (!Number.isSafeInteger(value.intervalMs) || value.intervalMs < 1000)
|
|
179
|
+
throw new Error("Schedule interval must be at least one second");
|
|
180
|
+
return store.saveSchedule(value);
|
|
181
|
+
},
|
|
182
|
+
ingest: async ({
|
|
183
|
+
source,
|
|
184
|
+
eventId,
|
|
185
|
+
kind,
|
|
186
|
+
body,
|
|
187
|
+
headers
|
|
188
|
+
}) => {
|
|
189
|
+
if (body.byteLength > maxBodyBytes)
|
|
190
|
+
throw new Error("Agent inbox event body is too large");
|
|
191
|
+
const verifier = verifiers[source];
|
|
192
|
+
if (!verifier)
|
|
193
|
+
throw new Error("No verifier for agent inbox source");
|
|
194
|
+
const checked = await verifier.verify({
|
|
195
|
+
source,
|
|
196
|
+
eventId,
|
|
197
|
+
kind,
|
|
198
|
+
body,
|
|
199
|
+
headers
|
|
200
|
+
});
|
|
201
|
+
if (!checked.valid || !checked.tenantId)
|
|
202
|
+
throw new AgentInboxVerificationError("Agent inbox event verification failed");
|
|
203
|
+
const subscriptions = await store.listSubscriptions({
|
|
204
|
+
tenantId: checked.tenantId,
|
|
205
|
+
source,
|
|
206
|
+
kind
|
|
207
|
+
});
|
|
208
|
+
const timestamp = new Date(now()).toISOString();
|
|
209
|
+
const output = [];
|
|
210
|
+
for (const subscription of subscriptions) {
|
|
211
|
+
const messageId = id();
|
|
212
|
+
const message = {
|
|
213
|
+
id: messageId,
|
|
214
|
+
subscriptionId: subscription.id,
|
|
215
|
+
target: subscription.target,
|
|
216
|
+
source,
|
|
217
|
+
sourceEventId: eventId,
|
|
218
|
+
kind,
|
|
219
|
+
encodedPayload: await codec.encode(checked.payload, {
|
|
220
|
+
tenantId: checked.tenantId,
|
|
221
|
+
messageId
|
|
222
|
+
}),
|
|
223
|
+
provenance: {
|
|
224
|
+
verifiedBy: verifier.id,
|
|
225
|
+
verifiedAt: timestamp,
|
|
226
|
+
...checked.proof !== undefined ? { proof: checked.proof } : {}
|
|
227
|
+
},
|
|
228
|
+
status: "pending",
|
|
229
|
+
attempts: 0,
|
|
230
|
+
maxAttempts: 5,
|
|
231
|
+
notBefore: timestamp,
|
|
232
|
+
createdAt: timestamp,
|
|
233
|
+
updatedAt: timestamp
|
|
234
|
+
};
|
|
235
|
+
output.push(await store.enqueue(message));
|
|
236
|
+
}
|
|
237
|
+
return output;
|
|
238
|
+
},
|
|
239
|
+
tickSchedule: async () => {
|
|
240
|
+
const timestamp = new Date(now()).toISOString();
|
|
241
|
+
const schedule = await store.claimDueSchedule({ now: timestamp });
|
|
242
|
+
if (!schedule)
|
|
243
|
+
return;
|
|
244
|
+
const occurrence = schedule.nextAt;
|
|
245
|
+
const nextAt = new Date(Date.parse(occurrence) + schedule.intervalMs).toISOString();
|
|
246
|
+
const messageId = id();
|
|
247
|
+
const message = await store.enqueue({
|
|
248
|
+
id: messageId,
|
|
249
|
+
subscriptionId: `schedule:${schedule.id}`,
|
|
250
|
+
target: schedule.target,
|
|
251
|
+
source: schedule.source,
|
|
252
|
+
sourceEventId: `${schedule.id}:${occurrence}`,
|
|
253
|
+
kind: schedule.kind,
|
|
254
|
+
encodedPayload: await codec.encode(schedule.payload, {
|
|
255
|
+
tenantId: schedule.target.tenantId,
|
|
256
|
+
messageId
|
|
257
|
+
}),
|
|
258
|
+
provenance: { verifiedBy: "agent-inbox:schedule", verifiedAt: timestamp },
|
|
259
|
+
status: "pending",
|
|
260
|
+
attempts: 0,
|
|
261
|
+
maxAttempts: schedule.maxAttempts,
|
|
262
|
+
notBefore: occurrence,
|
|
263
|
+
createdAt: timestamp,
|
|
264
|
+
updatedAt: timestamp
|
|
265
|
+
});
|
|
266
|
+
if (!await store.advanceSchedule({
|
|
267
|
+
id: schedule.id,
|
|
268
|
+
expectedNextAt: occurrence,
|
|
269
|
+
nextAt
|
|
270
|
+
}))
|
|
271
|
+
return;
|
|
272
|
+
return message;
|
|
273
|
+
},
|
|
274
|
+
workOne: async (workerId, handler, options = {}) => {
|
|
275
|
+
const claimedAt = now();
|
|
276
|
+
const message = await store.claim({
|
|
277
|
+
workerId,
|
|
278
|
+
now: new Date(claimedAt).toISOString(),
|
|
279
|
+
leaseExpiresAt: new Date(claimedAt + (options.leaseMs ?? 30000)).toISOString()
|
|
280
|
+
});
|
|
281
|
+
if (!message)
|
|
282
|
+
return;
|
|
283
|
+
try {
|
|
284
|
+
const payload = await codec.decode(message.encodedPayload, {
|
|
285
|
+
tenantId: message.target.tenantId,
|
|
286
|
+
messageId: message.id
|
|
287
|
+
});
|
|
288
|
+
await handler({ message, payload });
|
|
289
|
+
if (!await store.complete({
|
|
290
|
+
id: message.id,
|
|
291
|
+
workerId,
|
|
292
|
+
now: new Date(now()).toISOString()
|
|
293
|
+
}))
|
|
294
|
+
throw new Error("Agent inbox lease was lost");
|
|
295
|
+
return { ...message, status: "completed" };
|
|
296
|
+
} catch (error) {
|
|
297
|
+
const deadLetter = message.attempts >= message.maxAttempts;
|
|
298
|
+
const current = now();
|
|
299
|
+
await store.retry({
|
|
300
|
+
id: message.id,
|
|
301
|
+
workerId,
|
|
302
|
+
now: new Date(current).toISOString(),
|
|
303
|
+
notBefore: new Date(current + (options.retryDelayMs?.(message.attempts) ?? Math.min(300000, 1000 * 2 ** message.attempts))).toISOString(),
|
|
304
|
+
error: error instanceof Error ? error.message : "Agent inbox handler failed",
|
|
305
|
+
deadLetter
|
|
306
|
+
});
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
var createAgentRuntimeInboxHandler = (runtime) => async ({
|
|
312
|
+
message,
|
|
313
|
+
payload
|
|
314
|
+
}) => runtime.start({
|
|
315
|
+
actor: {
|
|
316
|
+
tenantId: message.target.tenantId,
|
|
317
|
+
userId: message.target.userId,
|
|
318
|
+
agentId: message.target.agentId
|
|
319
|
+
},
|
|
320
|
+
agent: message.target.agent,
|
|
321
|
+
goal: message.target.goal,
|
|
322
|
+
input: {
|
|
323
|
+
event: {
|
|
324
|
+
source: message.source,
|
|
325
|
+
kind: message.kind,
|
|
326
|
+
provenance: message.provenance
|
|
327
|
+
},
|
|
328
|
+
payload
|
|
329
|
+
},
|
|
330
|
+
idempotencyKey: `inbox:${message.id}`
|
|
331
|
+
});
|
|
332
|
+
// src/webhook.ts
|
|
333
|
+
var createAgentInboxWebhookHandler = (ingest, options) => async (request) => {
|
|
334
|
+
if (request.method !== "POST")
|
|
335
|
+
return new Response("method not allowed", { status: 405 });
|
|
336
|
+
const length = Number(request.headers.get("content-length") ?? 0);
|
|
337
|
+
if (length > (options.maxBodyBytes ?? 1048576))
|
|
338
|
+
return new Response("payload too large", { status: 413 });
|
|
339
|
+
const eventId = request.headers.get(options.eventIdHeader ?? "x-event-id");
|
|
340
|
+
const kind = request.headers.get(options.kindHeader ?? "x-event-type");
|
|
341
|
+
if (!eventId || !kind)
|
|
342
|
+
return new Response("missing event identity", { status: 400 });
|
|
343
|
+
try {
|
|
344
|
+
const body = new Uint8Array(await request.arrayBuffer());
|
|
345
|
+
if (body.byteLength > (options.maxBodyBytes ?? 1048576))
|
|
346
|
+
return new Response("payload too large", { status: 413 });
|
|
347
|
+
await ingest({
|
|
348
|
+
source: options.source,
|
|
349
|
+
eventId,
|
|
350
|
+
kind,
|
|
351
|
+
body,
|
|
352
|
+
headers: request.headers
|
|
353
|
+
});
|
|
354
|
+
return new Response(null, { status: 202 });
|
|
355
|
+
} catch (error) {
|
|
356
|
+
return error instanceof AgentInboxVerificationError ? new Response("verification failed", { status: 401 }) : new Response("inbox unavailable", { status: 503 });
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
export {
|
|
360
|
+
createPostgresAgentInboxStore,
|
|
361
|
+
createMemoryAgentInboxStore,
|
|
362
|
+
createAgentRuntimeInboxHandler,
|
|
363
|
+
createAgentInboxWebhookHandler,
|
|
364
|
+
createAgentInbox,
|
|
365
|
+
agentInboxPostgresSchemaSql,
|
|
366
|
+
AgentInboxVerificationError
|
|
367
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const manifest: import("@absolutejs/manifest").PackageManifest<unknown, never>;
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/manifest.ts
|
|
3
|
+
import { defineManifest } from "@absolutejs/manifest";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
var manifest = defineManifest()({
|
|
6
|
+
contract: 2,
|
|
7
|
+
identity: {
|
|
8
|
+
name: "@absolutejs/agent-inbox",
|
|
9
|
+
category: "automation",
|
|
10
|
+
tagline: "Wake agents from verified events, not ambient polling.",
|
|
11
|
+
description: "Durable verified webhooks, event subscriptions, interval schedules, encrypted payloads, leases, retries, dead letters, and agent-runtime handoff.",
|
|
12
|
+
docsUrl: "https://github.com/absolutejs/agent-inbox",
|
|
13
|
+
accent: "#06b6d4"
|
|
14
|
+
},
|
|
15
|
+
settings: Type.Object({}),
|
|
16
|
+
slots: {},
|
|
17
|
+
implements: [],
|
|
18
|
+
tools: {},
|
|
19
|
+
wiring: []
|
|
20
|
+
});
|
|
21
|
+
export {
|
|
22
|
+
manifest
|
|
23
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contract": 2,
|
|
3
|
+
"identity": {
|
|
4
|
+
"name": "@absolutejs/agent-inbox",
|
|
5
|
+
"category": "automation",
|
|
6
|
+
"tagline": "Wake agents from verified events, not ambient polling.",
|
|
7
|
+
"description": "Durable verified webhooks, event subscriptions, interval schedules, encrypted payloads, leases, retries, dead letters, and agent-runtime handoff.",
|
|
8
|
+
"docsUrl": "https://github.com/absolutejs/agent-inbox",
|
|
9
|
+
"accent": "#06b6d4"
|
|
10
|
+
},
|
|
11
|
+
"settings": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {}
|
|
14
|
+
},
|
|
15
|
+
"slots": {},
|
|
16
|
+
"implements": [],
|
|
17
|
+
"wiring": [],
|
|
18
|
+
"tools": {}
|
|
19
|
+
}
|
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AgentInboxStore } from "./types";
|
|
2
|
+
export type AgentInboxSqlResult<Row> = {
|
|
3
|
+
rows: Row[];
|
|
4
|
+
};
|
|
5
|
+
export type AgentInboxSqlTransaction = {
|
|
6
|
+
query<Row = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Promise<AgentInboxSqlResult<Row>>;
|
|
7
|
+
};
|
|
8
|
+
export type AgentInboxSqlClient = AgentInboxSqlTransaction & {
|
|
9
|
+
transaction<Value>(work: (tx: AgentInboxSqlTransaction) => Promise<Value>): Promise<Value>;
|
|
10
|
+
};
|
|
11
|
+
export declare const agentInboxPostgresSchemaSql: (schema?: string) => string;
|
|
12
|
+
export declare const createPostgresAgentInboxStore: ({ client, schema, }: {
|
|
13
|
+
client: AgentInboxSqlClient;
|
|
14
|
+
schema?: string;
|
|
15
|
+
}) => AgentInboxStore;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { AgentInboxCodec, AgentInboxMessage, AgentInboxStore, AgentInboxSubscription, AgentInboxVerifier, AgentSchedule } from "./types";
|
|
2
|
+
export declare class AgentInboxVerificationError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare const createAgentInbox: ({ store, verifiers, codec, now, id, maxBodyBytes, }: {
|
|
6
|
+
store: AgentInboxStore;
|
|
7
|
+
verifiers: Record<string, AgentInboxVerifier>;
|
|
8
|
+
codec?: AgentInboxCodec;
|
|
9
|
+
now?: () => number;
|
|
10
|
+
id?: () => string;
|
|
11
|
+
maxBodyBytes?: number;
|
|
12
|
+
}) => {
|
|
13
|
+
subscribe: (value: AgentInboxSubscription) => Promise<void>;
|
|
14
|
+
schedule: (value: AgentSchedule) => Promise<void>;
|
|
15
|
+
ingest: ({ source, eventId, kind, body, headers, }: {
|
|
16
|
+
source: string;
|
|
17
|
+
eventId: string;
|
|
18
|
+
kind: string;
|
|
19
|
+
body: Uint8Array;
|
|
20
|
+
headers: Headers;
|
|
21
|
+
}) => Promise<AgentInboxMessage[]>;
|
|
22
|
+
tickSchedule: () => Promise<AgentInboxMessage | undefined>;
|
|
23
|
+
workOne: (workerId: string, handler: (input: {
|
|
24
|
+
message: AgentInboxMessage;
|
|
25
|
+
payload: unknown;
|
|
26
|
+
}) => Promise<void>, options?: {
|
|
27
|
+
leaseMs?: number;
|
|
28
|
+
retryDelayMs?: (attempt: number) => number;
|
|
29
|
+
}) => Promise<{
|
|
30
|
+
status: "completed";
|
|
31
|
+
id: string;
|
|
32
|
+
subscriptionId: string;
|
|
33
|
+
target: import("./types").AgentInboxTarget;
|
|
34
|
+
source: string;
|
|
35
|
+
sourceEventId: string;
|
|
36
|
+
kind: string;
|
|
37
|
+
encodedPayload: unknown;
|
|
38
|
+
provenance: {
|
|
39
|
+
verifiedBy: string;
|
|
40
|
+
verifiedAt: string;
|
|
41
|
+
proof?: unknown;
|
|
42
|
+
};
|
|
43
|
+
attempts: number;
|
|
44
|
+
maxAttempts: number;
|
|
45
|
+
notBefore: string;
|
|
46
|
+
expiresAt?: string;
|
|
47
|
+
leaseOwner?: string;
|
|
48
|
+
leaseExpiresAt?: string;
|
|
49
|
+
lastError?: string;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
updatedAt: string;
|
|
52
|
+
} | undefined>;
|
|
53
|
+
};
|
|
54
|
+
export declare const createAgentRuntimeInboxHandler: (runtime: {
|
|
55
|
+
start(input: {
|
|
56
|
+
actor: {
|
|
57
|
+
tenantId: string;
|
|
58
|
+
userId: string;
|
|
59
|
+
agentId: string;
|
|
60
|
+
};
|
|
61
|
+
agent: AgentInboxMessage["target"]["agent"];
|
|
62
|
+
goal: string;
|
|
63
|
+
input: unknown;
|
|
64
|
+
idempotencyKey: string;
|
|
65
|
+
}): Promise<unknown>;
|
|
66
|
+
}) => ({ message, payload, }: {
|
|
67
|
+
message: AgentInboxMessage;
|
|
68
|
+
payload: unknown;
|
|
69
|
+
}) => Promise<unknown>;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export type AgentIdentityPin = {
|
|
2
|
+
descriptorId: string;
|
|
3
|
+
descriptorVersion: string;
|
|
4
|
+
descriptorDigest: string;
|
|
5
|
+
};
|
|
6
|
+
export type AgentInboxTarget = {
|
|
7
|
+
tenantId: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
agentId: string;
|
|
10
|
+
agent: AgentIdentityPin;
|
|
11
|
+
goal: string;
|
|
12
|
+
};
|
|
13
|
+
export type AgentInboxSubscription = {
|
|
14
|
+
id: string;
|
|
15
|
+
target: AgentInboxTarget;
|
|
16
|
+
source: string;
|
|
17
|
+
kinds: string[];
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
};
|
|
21
|
+
export type AgentInboxMessage = {
|
|
22
|
+
id: string;
|
|
23
|
+
subscriptionId: string;
|
|
24
|
+
target: AgentInboxTarget;
|
|
25
|
+
source: string;
|
|
26
|
+
sourceEventId: string;
|
|
27
|
+
kind: string;
|
|
28
|
+
encodedPayload: unknown;
|
|
29
|
+
provenance: {
|
|
30
|
+
verifiedBy: string;
|
|
31
|
+
verifiedAt: string;
|
|
32
|
+
proof?: unknown;
|
|
33
|
+
};
|
|
34
|
+
status: "pending" | "leased" | "completed" | "dead_letter";
|
|
35
|
+
attempts: number;
|
|
36
|
+
maxAttempts: number;
|
|
37
|
+
notBefore: string;
|
|
38
|
+
expiresAt?: string;
|
|
39
|
+
leaseOwner?: string;
|
|
40
|
+
leaseExpiresAt?: string;
|
|
41
|
+
lastError?: string;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
updatedAt: string;
|
|
44
|
+
};
|
|
45
|
+
export type AgentSchedule = {
|
|
46
|
+
id: string;
|
|
47
|
+
target: AgentInboxTarget;
|
|
48
|
+
source: string;
|
|
49
|
+
kind: string;
|
|
50
|
+
payload: unknown;
|
|
51
|
+
intervalMs: number;
|
|
52
|
+
nextAt: string;
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
maxAttempts: number;
|
|
55
|
+
createdAt: string;
|
|
56
|
+
};
|
|
57
|
+
export type AgentInboxStore = {
|
|
58
|
+
saveSubscription(value: AgentInboxSubscription): Promise<void>;
|
|
59
|
+
listSubscriptions(input: {
|
|
60
|
+
tenantId: string;
|
|
61
|
+
source: string;
|
|
62
|
+
kind: string;
|
|
63
|
+
}): Promise<AgentInboxSubscription[]>;
|
|
64
|
+
enqueue(value: AgentInboxMessage): Promise<AgentInboxMessage>;
|
|
65
|
+
claim(input: {
|
|
66
|
+
workerId: string;
|
|
67
|
+
now: string;
|
|
68
|
+
leaseExpiresAt: string;
|
|
69
|
+
}): Promise<AgentInboxMessage | undefined>;
|
|
70
|
+
complete(input: {
|
|
71
|
+
id: string;
|
|
72
|
+
workerId: string;
|
|
73
|
+
now: string;
|
|
74
|
+
}): Promise<boolean>;
|
|
75
|
+
retry(input: {
|
|
76
|
+
id: string;
|
|
77
|
+
workerId: string;
|
|
78
|
+
now: string;
|
|
79
|
+
notBefore: string;
|
|
80
|
+
error: string;
|
|
81
|
+
deadLetter: boolean;
|
|
82
|
+
}): Promise<boolean>;
|
|
83
|
+
saveSchedule(value: AgentSchedule): Promise<void>;
|
|
84
|
+
claimDueSchedule(input: {
|
|
85
|
+
now: string;
|
|
86
|
+
}): Promise<AgentSchedule | undefined>;
|
|
87
|
+
advanceSchedule(input: {
|
|
88
|
+
id: string;
|
|
89
|
+
expectedNextAt: string;
|
|
90
|
+
nextAt: string;
|
|
91
|
+
}): Promise<boolean>;
|
|
92
|
+
};
|
|
93
|
+
export type AgentInboxVerifier = {
|
|
94
|
+
id: string;
|
|
95
|
+
verify(input: {
|
|
96
|
+
source: string;
|
|
97
|
+
eventId: string;
|
|
98
|
+
kind: string;
|
|
99
|
+
body: Uint8Array;
|
|
100
|
+
headers: Headers;
|
|
101
|
+
}): Promise<{
|
|
102
|
+
valid: boolean;
|
|
103
|
+
tenantId?: string;
|
|
104
|
+
payload?: unknown;
|
|
105
|
+
proof?: unknown;
|
|
106
|
+
}>;
|
|
107
|
+
};
|
|
108
|
+
export type AgentInboxCodec = {
|
|
109
|
+
encode(value: unknown, context: {
|
|
110
|
+
tenantId: string;
|
|
111
|
+
messageId: string;
|
|
112
|
+
}): Promise<unknown>;
|
|
113
|
+
decode(value: unknown, context: {
|
|
114
|
+
tenantId: string;
|
|
115
|
+
messageId: string;
|
|
116
|
+
}): Promise<unknown>;
|
|
117
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const createAgentInboxWebhookHandler: (ingest: (input: {
|
|
2
|
+
source: string;
|
|
3
|
+
eventId: string;
|
|
4
|
+
kind: string;
|
|
5
|
+
body: Uint8Array;
|
|
6
|
+
headers: Headers;
|
|
7
|
+
}) => Promise<unknown>, options: {
|
|
8
|
+
source: string;
|
|
9
|
+
eventIdHeader?: string;
|
|
10
|
+
kindHeader?: string;
|
|
11
|
+
maxBodyBytes?: number;
|
|
12
|
+
}) => (request: Request) => Promise<Response>;
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@absolutejs/agent-inbox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Durable verified webhooks, event subscriptions, schedules, leases, retries, and dead letters for AI agent triggers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/absolutejs/agent-inbox.git"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"absolutejs": {
|
|
17
|
+
"manifestContract": 2
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./manifest": {
|
|
25
|
+
"types": "./dist/manifest.d.ts",
|
|
26
|
+
"import": "./dist/manifest.js"
|
|
27
|
+
},
|
|
28
|
+
"./manifest.json": "./dist/manifest.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"format": "prettier --write \"./**/*.{ts,json,md,yml}\"",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "bun test",
|
|
39
|
+
"build": "rm -rf dist && bun build src/index.ts src/manifest.ts --outdir dist --target=bun --external @absolutejs/manifest --external @sinclair/typebox && tsc -p tsconfig.build.json && absolute-manifest emit",
|
|
40
|
+
"check:package": "bun run typecheck && bun run test && bun run build"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@absolutejs/manifest": "^0.2.0",
|
|
44
|
+
"@sinclair/typebox": "^0.34.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/bun": "^1.3.14",
|
|
48
|
+
"prettier": "3.5.3",
|
|
49
|
+
"typescript": "5.8.3"
|
|
50
|
+
}
|
|
51
|
+
}
|